Skip to content

Commit

Permalink
Merge pull request #1196 from openedx/asheehan-edx/ENT-8503
Browse files Browse the repository at this point in the history
feat: adding budget group members tab
  • Loading branch information
alex-sheehan-edx authored Apr 15, 2024
2 parents 220f4fa + 0fec3c4 commit 99d093d
Show file tree
Hide file tree
Showing 18 changed files with 1,002 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,11 @@ import { BudgetDetailPageContext } from './BudgetDetailPageWrapper';
import { useBudgetDetailActivityOverview, useBudgetId, useSubsidyAccessPolicy } from './data';
import NoAssignableBudgetActivity from './empty-state/NoAssignableBudgetActivity';
import NoBnEBudgetActivity from './empty-state/NoBnEBudgetActivity';
import InviteMembersModalWrapper from './invite-modal/InviteMembersModalWrapper';

const BudgetDetailActivityTabContents = ({ enterpriseUUID, enterpriseFeatures }) => {
const isTopDownAssignmentEnabled = enterpriseFeatures.topDownAssignmentRealTimeLcm;
const isEnterpriseGroupsEnabled = enterpriseFeatures.enterpriseGroupsV1;
const {
inviteModalIsOpen, openInviteModal, closeInviteModal,
} = useContext(BudgetDetailPageContext);
const { openInviteModal } = useContext(BudgetDetailPageContext);
const { enterpriseOfferId, subsidyAccessPolicyId } = useBudgetId();
const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId);
const {
Expand Down Expand Up @@ -49,7 +46,6 @@ const BudgetDetailActivityTabContents = ({ enterpriseUUID, enterpriseFeatures })
return (
<>
{renderBnEActivity && (<NoBnEBudgetActivity openInviteModal={openInviteModal} />)}
<InviteMembersModalWrapper isOpen={inviteModalIsOpen} close={closeInviteModal} />
<BudgetDetailRedemptions />
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import LearnerCreditGroupMembersTable from './LearnerCreditGroupMembersTable';
import { useEnterpriseGroupMembersTableData, useBudgetId, useSubsidyAccessPolicy } from './data';

const BudgetDetailMembersTabContents = ({ enterpriseUUID, refresh }) => {
const { subsidyAccessPolicyId } = useBudgetId();
const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId);
const groupId = subsidyAccessPolicy.groupAssociations[0];
const {
isLoading,
enterpriseGroupMembersTableData,
fetchEnterpriseGroupMembersTableData,
} = useEnterpriseGroupMembersTableData({
enterpriseUUID,
subsidyAccessPolicyId,
groupId,
refresh,
});

return (
<>
<div className="mb-4">
<h4 className="mt-1">Budget Members</h4>
<p className="font-weight-light">
Members choose what to learn from the catalog and spend from the budget to enroll.
</p>
</div>
<LearnerCreditGroupMembersTable
isLoading={isLoading}
tableData={enterpriseGroupMembersTableData}
fetchTableData={fetchEnterpriseGroupMembersTableData}
/>
</>
);
};

const mapStateToProps = state => ({
enterpriseUUID: state.portalConfiguration.enterpriseId,
});

BudgetDetailMembersTabContents.propTypes = {
enterpriseUUID: PropTypes.string.isRequired,
refresh: PropTypes.bool.isRequired,
};

export default connect(mapStateToProps)(BudgetDetailMembersTabContents);
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { useNavigate, useParams } from 'react-router-dom';
Expand All @@ -8,28 +8,48 @@ import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils';
import {
BUDGET_DETAIL_ACTIVITY_TAB,
BUDGET_DETAIL_CATALOG_TAB,
BUDGET_DETAIL_MEMBERS_TAB,
} from './data/constants';
import { useBudgetDetailTabs, useBudgetId, useSubsidyAccessPolicy } from './data';
import {
useBudgetDetailTabs,
useBudgetId,
useEnterpriseGroupLearners,
useSubsidyAccessPolicy,
} from './data';
import { ROUTE_NAMES } from '../EnterpriseApp/data/constants';
import NotFoundPage from '../NotFoundPage';
import EVENT_NAMES from '../../eventTracking';

import InviteMembersModalWrapper from './invite-modal/InviteMembersModalWrapper';
import { BudgetDetailPageContext } from './BudgetDetailPageWrapper';

import BudgetDetailActivityTabContents from './BudgetDetailActivityTabContents';
import BudgetDetailCatalogTabContents from './BudgetDetailCatalogTabContents';
import BudgetDetailMembersTabContents from './BudgetDetailMembersTabContents';

const DEFAULT_TAB = BUDGET_DETAIL_ACTIVITY_TAB;

function isSupportedTabKey({ tabKey, isBudgetAssignable, enterpriseFeatures }) {
function isSupportedTabKey({
tabKey,
isBudgetAssignable,
enterpriseGroupLearners,
enterpriseFeatures,
}) {
const supportedTabs = [BUDGET_DETAIL_ACTIVITY_TAB];
if (enterpriseFeatures.topDownAssignmentRealTimeLcm && isBudgetAssignable) {
supportedTabs.push(BUDGET_DETAIL_CATALOG_TAB);
}
if (enterpriseGroupLearners?.count > 0) {
supportedTabs.push(BUDGET_DETAIL_MEMBERS_TAB);
}
return supportedTabs.includes(tabKey);
}

function getInitialTabKey(routeActiveTabKey, { isBudgetAssignable, enterpriseFeatures }) {
function getInitialTabKey(routeActiveTabKey, { isBudgetAssignable, enterpriseGroupLearners, enterpriseFeatures }) {
const isValidTabKey = isSupportedTabKey({
tabKey: routeActiveTabKey,
isBudgetAssignable,
enterpriseGroupLearners,
enterpriseFeatures,
});
if (!isValidTabKey) {
Expand All @@ -47,23 +67,31 @@ const BudgetDetailTabsAndRoutes = ({
const { budgetId, subsidyAccessPolicyId } = useBudgetId();
const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId);
const isBudgetAssignable = !!subsidyAccessPolicy?.isAssignable;

let groupUuid;
if (subsidyAccessPolicy?.groupAssociations?.length) {
[groupUuid] = subsidyAccessPolicy.groupAssociations;
}
const { data: enterpriseGroupLearners } = useEnterpriseGroupLearners(groupUuid);
const navigate = useNavigate();
const [activeTabKey, setActiveTabKey] = useState(getInitialTabKey(
routeActiveTabKey,
{ enterpriseFeatures, isBudgetAssignable },
{ enterpriseFeatures, enterpriseGroupLearners, isBudgetAssignable },
));

const {
inviteModalIsOpen, closeInviteModal,
} = useContext(BudgetDetailPageContext);

/**
* Ensure the active tab in the UI reflects the active tab in the URL.
*/
useEffect(() => {
const initialTabKey = getInitialTabKey(
routeActiveTabKey,
{ enterpriseFeatures, isBudgetAssignable },
{ enterpriseFeatures, enterpriseGroupLearners, isBudgetAssignable },
);
setActiveTabKey(initialTabKey);
}, [routeActiveTabKey, enterpriseFeatures, isBudgetAssignable]);
}, [routeActiveTabKey, enterpriseFeatures, isBudgetAssignable, enterpriseGroupLearners]);

const handleTabSelect = (nextActiveTabKey) => {
setActiveTabKey(nextActiveTabKey);
Expand All @@ -76,29 +104,43 @@ const BudgetDetailTabsAndRoutes = ({
);
};

const [refreshMembersTab, setRefreshMembersTab] = useState(false);

const tabs = useBudgetDetailTabs({
activeTabKey,
isBudgetAssignable,
enterpriseGroupLearners,
enterpriseFeatures,
refreshMembersTab,
ActivityTabElement: BudgetDetailActivityTabContents,
CatalogTabElement: BudgetDetailCatalogTabContents,
MembersTabElement: BudgetDetailMembersTabContents,
});

if (!isSupportedTabKey({
tabKey: routeActiveTabKey || activeTabKey,
isBudgetAssignable,
enterpriseGroupLearners,
enterpriseFeatures,
})) {
return <NotFoundPage />;
}

return (
<Tabs
activeKey={activeTabKey}
onSelect={handleTabSelect}
>
{tabs}
</Tabs>
<>
<InviteMembersModalWrapper
isOpen={inviteModalIsOpen}
close={closeInviteModal}
handleTabSelect={handleTabSelect}
setRefresh={setRefreshMembersTab}
refresh={refreshMembersTab}
/>
<Tabs
activeKey={activeTabKey}
onSelect={handleTabSelect}
>
{tabs}
</Tabs>
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from 'react';
import PropTypes from 'prop-types';
import { DataTable } from '@edx/paragon';
import TableTextFilter from './TableTextFilter';
import CustomDataTableEmptyState from './CustomDataTableEmptyState';
import MemberDetailsTableCell from './MemberDetailsTableCell';
import MemberStatusTableCell from './MemberStatusTableCell';
import MemberStatusTableColumnHeader from './MemberStatusTableColumnHeader';
import MemberEnrollmentsTableColumnHeader from './MemberEnrollmentsTableColumnHeader';

import {
MEMBERS_TABLE_PAGE_SIZE,
DEFAULT_PAGE,
} from './data';

const FilterStatus = (rest) => <DataTable.FilterStatus showFilteredFields={false} {...rest} />;

const LearnerCreditGroupMembersTable = ({
isLoading,
tableData,
fetchTableData,
}) => (
<DataTable
isSortable
isSelectable
manualSortBy
isPaginated
manualPagination
isFilterable
manualFilters
isLoading={isLoading}
defaultColumnValues={{ Filter: TableTextFilter }}
FilterStatusComponent={FilterStatus}
columns={[
{
Header: 'Member Details',
accessor: 'memberDetails',
Cell: MemberDetailsTableCell,
},
{
Header: MemberStatusTableColumnHeader,
accessor: 'status',
Cell: MemberStatusTableCell,
disableFilters: true,
},
{
Header: 'Recent action',
accessor: 'recentAction',
Cell: ({ row }) => row.original.recentAction,
disableFilters: true,
},
{
Header: MemberEnrollmentsTableColumnHeader,
accessor: 'memberEnrollment',
// TODO:
Cell: () => ('0'),
disableFilters: true,
},
]}
initialTableOptions={{
getRowId: row => row?.memberDetails.userEmail,
}}
initialState={{
pageSize: MEMBERS_TABLE_PAGE_SIZE,
pageIndex: DEFAULT_PAGE,
sortBy: [
{ id: 'memberDetails', desc: true },
],
filters: [],
}}
fetchData={fetchTableData}
data={tableData.results}
itemCount={tableData.itemCount}
pageCount={tableData.pageCount}
EmptyTableComponent={CustomDataTableEmptyState}
/>
);

LearnerCreditGroupMembersTable.propTypes = {
isLoading: PropTypes.bool.isRequired,
tableData: PropTypes.shape({
results: PropTypes.arrayOf(PropTypes.shape({
})),
itemCount: PropTypes.number.isRequired,
pageCount: PropTypes.number.isRequired,
}).isRequired,
fetchTableData: PropTypes.func.isRequired,
};

export default LearnerCreditGroupMembersTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
IconButton,
Stack,
Icon,
} from '@edx/paragon';
import {
Person,
} from '@edx/paragon/icons';

const MemberDetailsTableCell = ({
row,
}) => {
let memberDetails;
let memberDetailIcon = (
<IconButton
isActive
invertColors
src={Person}
iconAs={Icon}
className="border rounded-circle mr-3"
alt="members detail column icon"
/>
);
if (row.original.status === 'removed') {
memberDetails = (
<div className="mb-n3">
<p className="text-danger-900 font-weight-bold text-uppercase x-small mb-0">
FORMER MEMBER
</p>
<p>{row.original.memberDetails.userEmail}</p>
</div>
);
memberDetailIcon = (
<IconButton
isActive
invertColors
src={Person}
iconAs={Icon}
className="border border-gray-400 rounded-circle mr-3"
alt="members detail column icon"
style={{ opacity: 0.2 }}
/>
);
} else if (row.original.memberDetails.userName) {
memberDetails = (
<div className="mb-n3">
<p className="font-weight-bold mb-0">
{row.original.memberDetails.userName}
</p>
<p>{row.original.memberDetails.userEmail}</p>
</div>
);
} else {
memberDetails = (
<p className="align-middle mb-0">
{row.original.memberDetails.userEmail}
</p>
);
}
return (
<Stack gap={0} direction="horizontal">
{memberDetailIcon}
{memberDetails}
</Stack>
);
};

MemberDetailsTableCell.propTypes = {
row: PropTypes.shape({
original: PropTypes.shape({
memberDetails: PropTypes.shape({
userEmail: PropTypes.string.isRequired,
userName: PropTypes.string,
}),
status: PropTypes.string,
recentAction: PropTypes.string.isRequired,
memberEnrollments: PropTypes.string,
}).isRequired,
}).isRequired,
};

export default MemberDetailsTableCell;
Loading

0 comments on commit 99d093d

Please sign in to comment.