diff --git a/src/components/global/FilterPopover/FilterRolesPopover.tsx b/src/components/global/FilterPopover/FilterRolesPopover.tsx index 69c4c2bc..0310f3ee 100644 --- a/src/components/global/FilterPopover/FilterRolesPopover.tsx +++ b/src/components/global/FilterPopover/FilterRolesPopover.tsx @@ -1,100 +1,298 @@ -import React, { useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; +import { + Autocomplete, + FormControl, + FormControlLabel, + ListItem, + Radio, + RadioGroup, + TextField, +} from '@mui/material'; +import { + MdOutlineKeyboardArrowDown, + MdOutlineKeyboardArrowUp, +} from 'react-icons/md'; +import Loading from '../Loading'; + import TcButton from '../../shared/TcButton'; import TcPopover from '../../shared/TcPopover'; -import TcSelect from '../../shared/TcSelect'; import TcCheckbox from '../../shared/TcCheckbox'; -import { FormControl, FormControlLabel, ListItem } from '@mui/material'; -// import TcAutocomplete from '../../shared/TcAutocomplete'; - -const mockRoles = [ - { name: 'Admin', id: 1, color: '#20d321' }, - { name: 'Supporter', id: 2, color: '#20d321' }, -]; -const autoCompleteOptions = [ - { label: 'Role 1', value: 'role1' }, - { label: 'Role 2', value: 'role2' }, -]; - -function FilterRolesPopover() { + +import useAppStore from '../../../store/useStore'; +import { useToken } from '../../../context/TokenContext'; +import { FetchedData, IRoles } from '../../../utils/interfaces'; +import TcText from '../../shared/TcText'; +import { IRolesPayload } from '../../pages/statistics/memberBreakdowns/CustomTable'; + +interface Payload { + allRoles: boolean; + exclude?: string[]; + include?: string[]; +} + +function createPayload( + includeExclude: 'include' | 'exclude', + selectedRoles: string[] +): Payload { + if (!Array.isArray(selectedRoles)) { + throw new Error('selectedRoles must be an array of strings'); + } + + const payload: Payload = { + allRoles: false, + }; + + if (includeExclude === 'exclude') { + payload.exclude = selectedRoles; + } else { + payload.include = selectedRoles; + } + + return payload; +} +interface IFilterRolesPopover { + handleSelectedRoles: (payload: IRolesPayload) => void; +} +function FilterRolesPopover({ handleSelectedRoles }: IFilterRolesPopover) { + const { retrievePlatformProperties } = useAppStore(); + const { community } = useToken(); + const platformId = community?.platforms[0]?.id; + + const scrollableDivRef = useRef(null); + const [popoverOpen, setPopoverOpen] = useState(false); - const [selectedRoles, setSelectedRoles] = useState(new Set()); + const [page, setPage] = useState(1); + const [loading, setLoading] = useState(false); + const [roles, setRoles] = useState({ + limit: 8, + page: 1, + results: [], + totalPages: 0, + totalResults: 0, + }); + const [selectedRoles, setSelectedRoles] = useState([]); const [anchorEl, setAnchorEl] = useState(null); - const [autoCompleteValue, setAutoCompleteValue] = useState(null); + + const [includeExclude, setIncludeExclude] = useState<'include' | 'exclude'>( + 'include' + ); + + const handleIncludeExcludeChange = ( + event: React.ChangeEvent + ) => { + const newValue = event.target.value; + + if (newValue === 'include' || newValue === 'exclude') { + setIncludeExclude(newValue); + } + }; + + const isRolesPopupOpen = Boolean(anchorEl); const handleButtonClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); setPopoverOpen(true); }; - const handleAutoCompleteChange = (newValue: any) => { - setAutoCompleteValue(newValue); - }; - const handleClosePopover = () => { setPopoverOpen(false); setAnchorEl(null); }; - const handleRoleChange = (roleId: number, isChecked: boolean) => { - setSelectedRoles((prevSelectedRoles) => { - const newSelectedRoles = new Set(prevSelectedRoles); - if (isChecked) { - newSelectedRoles.add(roleId); - } else { - newSelectedRoles.delete(roleId); - } - return newSelectedRoles; + const loadMoreRoles = async () => { + setLoading(true); + const nextPage = page + 1; + setPage(nextPage); + + const fetchedRoles = await retrievePlatformProperties({ + platformId, + property: 'role', + page: nextPage, + limit: roles.limit, + }); + + const newRoles = fetchedRoles.results.filter( + (fetchedRole: IRoles) => + !roles.results.some((role) => role.id === fetchedRole.id) + ); + + setRoles((prevRoles) => ({ + ...fetchedRoles, + results: [...prevRoles.results, ...newRoles], + })); + setLoading(false); + }; + + const fetchRoles = async (name?: string) => { + const fetchedRoles = await retrievePlatformProperties({ + platformId, + property: 'role', + page: roles?.page, + limit: roles?.limit, }); + setRoles(fetchedRoles); }; + const handleScroll = () => { + const element = scrollableDivRef.current; + if (!element) return; + + const hasReachedBottom = + element.scrollHeight - element.scrollTop === element.clientHeight; + const hasMoreRolesToLoad = roles.page * roles.limit <= roles.totalResults; + + if (hasReachedBottom && hasMoreRolesToLoad) { + loadMoreRoles(); + } + }; + + const handleSelectedRole = ( + event: React.ChangeEvent, + role: IRoles + ) => { + const roleId = role.roleId; + setSelectedRoles((prev) => + event.target.checked + ? [...prev, roleId] + : prev.filter((id) => id !== roleId) + ); + }; + + const handleAutocompleteChange = ( + event: React.ChangeEvent<{}>, + newValue: IRoles[] + ) => { + setSelectedRoles(newValue.map((role) => role.roleId)); + }; + + useEffect(() => { + let payload; + + if (selectedRoles.length !== 0) { + payload = createPayload(includeExclude, selectedRoles); + } else { + payload = { allRoles: true }; + } + + handleSelectedRoles(payload); + }, [selectedRoles, includeExclude]); + + useEffect(() => { + if (!platformId) return; + fetchRoles(); + }, [platformId]); + + const renderRoleItem = (option: IRoles) => ( +
+ +
{option.name}
+
+ ); + return ( <> - + + ) : ( + + ) + } + sx={{ + width: 'auto', + }} + onClick={handleButtonClick} + /> - - + {selectedRoles.length > 0 ? ( + + + } + label={} + className="w-1/2" + /> + } + label={} + /> + + + ) : ( +
+ )} + + option.name} + value={roles.results.filter((role) => + selectedRoles.includes(role.roleId) + )} + onChange={handleAutocompleteChange} + renderOption={(props, option) => ( +
  • {renderRoleItem(option)}
  • + )} + renderTags={(value, getTagProps) => + value.map((option, index) => renderRoleItem(option)) + } + filterSelectedOptions + renderInput={(params) => ( + + )} />
    - {/* */} -
    - {mockRoles.map((role) => ( - +
    + + {roles?.results?.map((role: IRoles) => ( + - handleRoleChange(role.id, e.target.checked) - } + checked={selectedRoles.includes(role.roleId)} + onChange={(e) => handleSelectedRole(e, role)} /> } - label={ -
    - -
    {role.name}
    -
    - } + label={renderRoleItem(role)} />
    ))} + {loading ? : ''}
    } diff --git a/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx b/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx index 16ca3e10..13b8ec93 100644 --- a/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx +++ b/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx @@ -29,13 +29,18 @@ import { Row, IActivityCompositionOptions, } from '../../../../utils/interfaces'; -import { IUser } from '../../../../utils/types'; import { conf } from '../../../../configs'; import Loading from '../../../global/Loading'; import useAppStore from '../../../../store/useStore'; -import { StorageService } from '../../../../services/StorageService'; import CustomDialogDetail from './CustomDialogDetail'; import router from 'next/router'; +import FilterRolesPopover from '../../../global/FilterPopover/FilterRolesPopover'; + +export interface IRolesPayload { + allRoles: boolean; + exclude?: string[]; + include?: string[]; +} interface CustomTableProps { data: Row[]; @@ -43,7 +48,7 @@ interface CustomTableProps { isLoading: boolean; breakdownName?: string; activityCompositionOptions: IActivityCompositionOptions[]; - handleRoleSelectionChange: (selectedRoles: string[]) => void; + handleRoleSelectionChange: (rolesPayload: IRolesPayload) => void; handleActivityOptionSelectionChange: (selectedRoles: string[]) => void; handleJoinedAtChange: (joinedAt: string) => void; handleUsernameChange: (userName: string) => void; @@ -54,44 +59,26 @@ const CustomTable: React.FC = ({ columns, isLoading, breakdownName, - handleRoleSelectionChange, handleActivityOptionSelectionChange, + handleRoleSelectionChange, handleJoinedAtChange, handleUsernameChange, activityCompositionOptions, }) => { - const { getRoles, roles } = useAppStore(); - // useEffect(() => { - // const user = StorageService.readLocalStorage('user'); - - // if (!user) { - // return; - // } - - // const { guild } = user; - // getRoles(guild.guildId); - // }, []); + const { roles } = useAppStore(); const [anchorElRoles, setAnchorElRoles] = useState( null ); const [anchorElActivity, setAnchorElActivity] = useState(null); - const [selectedRoles, setSelectedRoles] = useState([]); - useEffect(() => { - setSelectedRoles(roles.map((role: IRoles) => role.roleId)); - }, [roles]); - const [selectAllRoles, setSelectAllRoles] = useState(true); + const [selectedActivityOptions, setSelectedActivityOptions] = useState< string[] >(activityCompositionOptions.map((option) => option.value)); const [selectAllActivityOptions, setSelectAllActivityOptions] = useState(true); - const handleOpenRolesPopup = (event: React.MouseEvent) => { - setAnchorElRoles(event.currentTarget); - }; - const handleOpenActivityPopup = ( event: React.MouseEvent ) => { @@ -104,33 +91,12 @@ const CustomTable: React.FC = ({ setAnchorElJoinedAt(null); }; - const isRolesPopupOpen = Boolean(anchorElRoles); const isActivityPopupOpen = Boolean(anchorElActivity); - const handleSelectAllRoles = (event: React.ChangeEvent) => { - if (event.target.checked) { - const allRoleNames = roles.map((role: IRoles) => role.roleId); - setSelectedRoles(allRoleNames); - } else { - setSelectedRoles([]); - } - setSelectAllRoles(event.target.checked); - }; - - const handleSelectRole = (event: React.ChangeEvent) => { - const roleName = event.target.value; - const updatedSelectedRoles = selectedRoles.includes(roleName) - ? selectedRoles.filter((role) => role !== roleName) - : [...selectedRoles, roleName]; - - setSelectedRoles(updatedSelectedRoles); - setSelectAllRoles(updatedSelectedRoles.length === roles.length); + const handleSelectedRoles = (payload: IRolesPayload) => { + handleRoleSelectionChange(payload); }; - useEffect(() => { - handleRoleSelectionChange(selectedRoles); - }, [selectedRoles]); - useEffect(() => { handleActivityOptionSelectionChange(selectedActivityOptions); }, [selectedActivityOptions]); @@ -282,79 +248,9 @@ const CustomTable: React.FC = ({ > {column.id === 'roles' ? ( <> - - -
    - - } - label={'All Roles'} - /> -

    Show members with tags:

    - {roles.map((role: IRoles) => ( - - - } - label={ -
    - -
    {role.name}
    -
    - } - /> -
    - ))} -
    -
    + ) : column.id === 'activityComposition' ? ( <> @@ -554,7 +450,11 @@ const CustomTable: React.FC = ({ {column.id === 'username' ? (

    diff --git a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx index 994a69c0..c830e788 100644 --- a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx +++ b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx @@ -1,8 +1,9 @@ import { useState, useEffect, useRef } from 'react'; import useAppStore from '../../../../../store/useStore'; -import CustomTable from '../CustomTable'; +import CustomTable, { IRolesPayload } from '../CustomTable'; import { Column, + FetchedData, IActivityCompositionOptions, } from '../../../../../utils/interfaces'; import CustomPagination from '../CustomPagination'; @@ -42,22 +43,18 @@ export default function ActiveMemberBreakdown() { const [isExpanded, toggleExpanded] = useState(false); const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); - const [roles, setRoles] = useState([]); + const [roles, setRoles] = useState(); const [activityComposition, setActivityComposition] = useState( options.map((option) => option.value) ); const [username, setUsername] = useState(''); const [sortBy, setSortBy] = useState('desc'); - const [fetchedData, setFetchedData] = useState<{ - limit?: string | number; - page?: string | number; - results: any[]; - totalPages: number; - totalResults: number; - }>({ + const [fetchedData, setFetchedData] = useState({ + limit: 10, + page: 1, results: [], - totalResults: 0, totalPages: 0, + totalResults: 0, }); const platformId = community?.platforms[0]?.id; @@ -72,7 +69,6 @@ export default function ActiveMemberBreakdown() { if (!platformId) { return; } - console.log(platformId, 'test log'); setLoading(true); const fetchData = async () => { @@ -126,8 +122,8 @@ export default function ActiveMemberBreakdown() { } }, [router.query]); - const handleRoleSelectionChange = (selectedRoles: string[]) => { - setRoles(selectedRoles); + const handleRoleSelectionChange = (rolesPayload: IRolesPayload) => { + setRoles(rolesPayload); }; const handleActivityOptionSelectionChange = (selectedOptions: string[]) => { diff --git a/src/components/pages/statistics/memberBreakdowns/disengagedMembersComposition/DisengagedMembersCompositionBreakdown.tsx b/src/components/pages/statistics/memberBreakdowns/disengagedMembersComposition/DisengagedMembersCompositionBreakdown.tsx index d750d6be..e34bab2d 100644 --- a/src/components/pages/statistics/memberBreakdowns/disengagedMembersComposition/DisengagedMembersCompositionBreakdown.tsx +++ b/src/components/pages/statistics/memberBreakdowns/disengagedMembersComposition/DisengagedMembersCompositionBreakdown.tsx @@ -1,8 +1,9 @@ import { useState, useEffect, useRef } from 'react'; import useAppStore from '../../../../../store/useStore'; -import CustomTable from '../CustomTable'; +import CustomTable, { IRolesPayload } from '../CustomTable'; import { Column, + FetchedData, IActivityCompositionOptions, } from '../../../../../utils/interfaces'; import CustomPagination from '../CustomPagination'; @@ -53,22 +54,18 @@ export default function DisengagedMembersCompositionBreakdown() { const [isExpanded, toggleExpanded] = useState(false); const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); - const [roles, setRoles] = useState([]); + const [roles, setRoles] = useState(); const [disengagedComposition, setDisengagedComposition] = useState( options.map((option) => option.value) ); const [username, setUsername] = useState(''); const [sortBy, setSortBy] = useState('desc'); - const [fetchedData, setFetchedData] = useState<{ - limit?: string | number; - page?: string | number; - results: any[]; - totalPages: number; - totalResults: number; - }>({ + const [fetchedData, setFetchedData] = useState({ + limit: 10, + page: 1, results: [], - totalResults: 0, totalPages: 0, + totalResults: 0, }); const platformId = community?.platforms[0]?.id; @@ -136,8 +133,8 @@ export default function DisengagedMembersCompositionBreakdown() { } }, [router.query]); - const handleRoleSelectionChange = (selectedRoles: string[]) => { - setRoles(selectedRoles); + const handleRoleSelectionChange = (rolesPayload: IRolesPayload) => { + setRoles(rolesPayload); }; const handleActivityOptionSelectionChange = (selectedOptions: string[]) => { diff --git a/src/components/pages/statistics/memberBreakdowns/onboardingMembers/OnboardingMembersBreakdown.tsx b/src/components/pages/statistics/memberBreakdowns/onboardingMembers/OnboardingMembersBreakdown.tsx index ef0974eb..57c1c62a 100644 --- a/src/components/pages/statistics/memberBreakdowns/onboardingMembers/OnboardingMembersBreakdown.tsx +++ b/src/components/pages/statistics/memberBreakdowns/onboardingMembers/OnboardingMembersBreakdown.tsx @@ -1,8 +1,9 @@ import { useState, useEffect, useRef } from 'react'; import useAppStore from '../../../../../store/useStore'; -import CustomTable from '../CustomTable'; +import CustomTable, { IRolesPayload } from '../CustomTable'; import { Column, + FetchedData, IActivityCompositionOptions, } from '../../../../../utils/interfaces'; import CustomPagination from '../CustomPagination'; @@ -41,22 +42,18 @@ export default function OnboardingMembersBreakdown() { const [isExpanded, toggleExpanded] = useState(false); const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); - const [roles, setRoles] = useState([]); + const [roles, setRoles] = useState(); const [onboardingComposition, setOnboardingComposition] = useState( options.map((option) => option.value) ); const [username, setUsername] = useState(''); const [sortBy, setSortBy] = useState('desc'); - const [fetchedData, setFetchedData] = useState<{ - limit?: string | number; - page?: string | number; - results: any[]; - totalPages: number; - totalResults: number; - }>({ + const [fetchedData, setFetchedData] = useState({ + limit: 10, + page: 1, results: [], - totalResults: 0, totalPages: 0, + totalResults: 0, }); const platformId = community?.platforms[0]?.id; @@ -123,8 +120,9 @@ export default function OnboardingMembersBreakdown() { } } }, [router.query]); - const handleRoleSelectionChange = (selectedRoles: string[]) => { - setRoles(selectedRoles); + + const handleRoleSelectionChange = (rolesPayload: IRolesPayload) => { + setRoles(rolesPayload); }; const handleActivityOptionSelectionChange = (selectedOptions: string[]) => { diff --git a/src/components/shared/TcPopover/TcPopover.tsx b/src/components/shared/TcPopover/TcPopover.tsx index dc0a3524..ea3c93b3 100644 --- a/src/components/shared/TcPopover/TcPopover.tsx +++ b/src/components/shared/TcPopover/TcPopover.tsx @@ -20,9 +20,9 @@ */ import React from 'react'; -import { Popover } from '@mui/material'; +import { Popover, PopoverProps } from '@mui/material'; -interface TcPopoverProps { +interface TcPopoverProps extends PopoverProps { open: boolean; anchorEl: HTMLButtonElement | null; content: React.ReactNode; diff --git a/src/store/slices/breakdownsSlice.ts b/src/store/slices/breakdownsSlice.ts index 007a8e62..1b73f423 100644 --- a/src/store/slices/breakdownsSlice.ts +++ b/src/store/slices/breakdownsSlice.ts @@ -1,6 +1,7 @@ import { StateCreator } from 'zustand'; import { axiosInstance } from '../../axiosInstance'; import IBreakdown from '../types/IBreakdown'; +import { IRolesPayload } from '../../components/pages/statistics/memberBreakdowns/CustomTable'; const createBreakdownsSlice: StateCreator = (set, get) => ({ isActiveMembersBreakdownLoading: false, @@ -11,7 +12,7 @@ const createBreakdownsSlice: StateCreator = (set, get) => ({ getActiveMemberCompositionTable: async ( platformId: string, activityComposition: string[], - roles: string[], + roles: IRolesPayload, username?: string, sortBy?: string, page?: number, @@ -39,17 +40,13 @@ const createBreakdownsSlice: StateCreator = (set, get) => ({ params.append('activityComposition', value); }); - requestData.roles.forEach((value) => { - params.append('roles', value); - }); - if (username) { params.append('ngu', username); } const url = `/member-activity/${platformId}/active-members-composition-table?${params.toString()}`; - const { data } = await axiosInstance.post(url); + const { data } = await axiosInstance.post(url, roles); return data; } catch (error) {} @@ -57,7 +54,7 @@ const createBreakdownsSlice: StateCreator = (set, get) => ({ getOnboardingMemberCompositionTable: async ( platformId: string, activityComposition: string[], - roles: string[], + roles: IRolesPayload, username?: string, sortBy?: string, page?: number, @@ -85,17 +82,13 @@ const createBreakdownsSlice: StateCreator = (set, get) => ({ params.append('activityComposition', value); }); - requestData.roles.forEach((value) => { - params.append('roles', value); - }); - if (username) { params.append('ngu', username); } const url = `/member-activity/${platformId}/active-members-onboarding-table?${params.toString()}`; - const { data } = await axiosInstance.post(url); + const { data } = await axiosInstance.post(url, roles); return data; } catch (error) {} @@ -103,7 +96,7 @@ const createBreakdownsSlice: StateCreator = (set, get) => ({ getDisengagedMembersCompositionTable: async ( platformId: string, activityComposition: string[], - roles: string[], + roles: IRolesPayload, username?: string, sortBy?: string, page?: number, @@ -131,17 +124,13 @@ const createBreakdownsSlice: StateCreator = (set, get) => ({ params.append('activityComposition', value); }); - requestData.roles.forEach((value) => { - params.append('roles', value); - }); - if (username) { params.append('ngu', username); } const url = `/member-activity/${platformId}/disengaged-members-composition-table?${params.toString()}`; - const { data } = await axiosInstance.post(url); + const { data } = await axiosInstance.post(url, roles); return data; } catch (error) {} diff --git a/src/store/types/IBreakdown.ts b/src/store/types/IBreakdown.ts index bf7d77ca..6932e793 100644 --- a/src/store/types/IBreakdown.ts +++ b/src/store/types/IBreakdown.ts @@ -1,3 +1,5 @@ +import { IRolesPayload } from '../../components/pages/statistics/memberBreakdowns/CustomTable'; + export default interface IBreakdown { isActiveMembersBreakdownLoading: boolean; isOnboardingMembersBreakdownLoading: boolean; @@ -7,7 +9,7 @@ export default interface IBreakdown { getActiveMemberCompositionTable: ( platformId: string, activityComposition: string[], - roles: string[], + roles: IRolesPayload, username?: string, sortBy?: string, page?: number, @@ -16,7 +18,7 @@ export default interface IBreakdown { getOnboardingMemberCompositionTable: ( platformId: string, activityComposition: string[], - roles: string[], + roles: IRolesPayload, username?: string, sortBy?: string, page?: number, @@ -25,7 +27,7 @@ export default interface IBreakdown { getDisengagedMembersCompositionTable: ( platformId: string, activityComposition: string[], - roles: string[], + roles: IRolesPayload, username?: string, sortBy?: string, page?: number, diff --git a/src/store/types/IPlatform.ts b/src/store/types/IPlatform.ts index 4252363f..6ad1e5f5 100644 --- a/src/store/types/IPlatform.ts +++ b/src/store/types/IPlatform.ts @@ -48,4 +48,12 @@ export default interface IPlatfrom { id, metadata: { selectedChannels, period, analyzerStartedAt }, }: IPatchPlatformInput) => void; + retrievePlatformProperties: ({ + platformId, + property, + name, + sortBy, + page, + limit, + }: IRetrivePlatformRolesOrChannels) => void; } diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index ec4f3994..d717b1ba 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -27,6 +27,8 @@ export interface IRoles { roleId: string; color: number | string; name: string; + deletedAt: string; + id: number | string; } export interface IUserProfile {