-
Notifications
You must be signed in to change notification settings - Fork 32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: feat: Conditionally filter the budget enrollments #1028
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ | |
import { ROUTE_NAMES } from './data/constants'; | ||
import BulkEnrollmentResultsDownloadPage from '../BulkEnrollmentResultsDownloadPage'; | ||
import LearnerCreditManagement from '../learner-credit-management'; | ||
import BudgetDetailPage from '../learner-credit-management/BudgetDetailPage'; | ||
import { EnterpriseSubsidiesContext } from '../EnterpriseSubsidiesContext'; | ||
import ContentHighlights from '../ContentHighlights'; | ||
|
||
|
@@ -105,6 +106,11 @@ | |
/> | ||
)} | ||
|
||
<Route | ||
exact | ||
path={`${baseUrl}/admin/${ROUTE_NAMES.learnerCredit}/:id`} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: perhaps |
||
component={BudgetDetailPage} | ||
/> | ||
{enableContentHighlightsPage && ( | ||
<Route | ||
path={`${baseUrl}/admin/${ROUTE_NAMES.contentHighlights}`} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
import React, { useState } from 'react'; | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import dayjs from 'dayjs'; | ||
import { | ||
|
@@ -7,58 +7,45 @@ | |
Stack, | ||
Row, | ||
Col, | ||
Breadcrumb, | ||
} from '@edx/paragon'; | ||
|
||
import { useOfferRedemptions, useOfferSummary } from './data/hooks'; | ||
import { useHistory } from 'react-router-dom'; | ||
import { useOfferSummary } from './data/hooks'; | ||
import LearnerCreditAggregateCards from './LearnerCreditAggregateCards'; | ||
import LearnerCreditAllocationTable from './LearnerCreditAllocationTable'; | ||
import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; | ||
import { ROUTE_NAMES } from "../EnterpriseApp/data/constants"; | ||
|
||
const BudgetCard = ({ | ||
offer, | ||
enterpriseUUID, | ||
enterpriseSlug, | ||
enableLearnerPortal, | ||
}) => { | ||
const { | ||
start, | ||
end, | ||
} = offer; | ||
const history = useHistory(); | ||
|
||
const { | ||
isLoading: isLoadingOfferSummary, | ||
offerSummary, | ||
} = useOfferSummary(enterpriseUUID, offer); | ||
|
||
const { | ||
isLoading: isLoadingOfferRedemptions, | ||
offerRedemptions, | ||
fetchOfferRedemptions, | ||
} = useOfferRedemptions(enterpriseUUID, offer?.id); | ||
const [detailPage, setDetailPage] = useState(false); | ||
const [activeLabel, setActiveLabel] = useState(''); | ||
const links = [ | ||
{ label: 'Budgets', url: `/${enterpriseSlug}/admin/${ROUTE_NAMES.learnerCredit}` }, | ||
]; | ||
const formattedStartDate = dayjs(start).format('MMMM D, YYYY'); | ||
const formattedExpirationDate = dayjs(end).format('MMMM D, YYYY'); | ||
const navigateToBudgetRedemptions = (budgetType) => { | ||
setDetailPage(true); | ||
links.push({ label: budgetType, url: `/${enterpriseSlug}/admin/learner-credit` }); | ||
setActiveLabel(budgetType); | ||
const navigateToBudgetRedemptions = (id) => { | ||
history.push(`/${enterpriseSlug}/admin/${ROUTE_NAMES.learnerCredit}/${id}`); | ||
}; | ||
|
||
const renderActions = (budgetType) => ( | ||
const renderActions = (id) => ( | ||
<Button | ||
data-testid="view-budget" | ||
onClick={() => navigateToBudgetRedemptions(budgetType)} | ||
onClick={() => navigateToBudgetRedemptions(id)} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] I would recommend treating this <Button
data-testid="view-budget"
as={Link}
to={`/${enterpriseSlug}/admin/${ROUTE_NAMES.learnerCredit}/${id}`}
>
View budget
</Button> |
||
> | ||
View Budget | ||
</Button> | ||
); | ||
|
||
const renderCardHeader = (budgetType) => { | ||
const renderCardHeader = (budgetType, id) => { | ||
const subtitle = ( | ||
<div className="d-flex flex-wrap align-items-center"> | ||
<span data-testid="offer-date"> | ||
|
@@ -73,7 +60,7 @@ | |
subtitle={subtitle} | ||
actions={( | ||
<div> | ||
{renderActions(budgetType)} | ||
{renderActions(id)} | ||
</div> | ||
)} | ||
/> | ||
|
@@ -112,42 +99,22 @@ | |
|
||
return ( | ||
<Stack> | ||
<Row className="m-3"> | ||
<Col xs="12"> | ||
<Breadcrumb | ||
ariaLabel="Breadcrumb" | ||
links={links} | ||
activeLabel={activeLabel} | ||
/> | ||
</Col> | ||
</Row> | ||
{!detailPage | ||
? ( | ||
<> | ||
{renderCardAggregate()} | ||
<h2>Budgets</h2> | ||
<Card | ||
orientation="horizontal" | ||
> | ||
<Card.Body> | ||
<Stack gap={4}> | ||
{renderCardHeader('Overview')} | ||
{renderCardSection(offerSummary?.remainingFunds, offerSummary?.redeemedFunds)} | ||
</Stack> | ||
</Card.Body> | ||
</Card> | ||
</> | ||
) | ||
: ( | ||
<LearnerCreditAllocationTable | ||
isLoading={isLoadingOfferRedemptions} | ||
tableData={offerRedemptions} | ||
fetchTableData={fetchOfferRedemptions} | ||
enterpriseUUID={enterpriseUUID} | ||
enterpriseSlug={enterpriseSlug} | ||
enableLearnerPortal={enableLearnerPortal} | ||
/> | ||
)} | ||
|
||
<> | ||
{renderCardAggregate()} | ||
<h2>Budgets</h2> | ||
<Card | ||
orientation="horizontal" | ||
> | ||
<Card.Body> | ||
<Stack gap={4}> | ||
{/*{renderCardHeader('Overview', 'f25682b2-62ce-42a7-b596-8acc86811cc9')}*/} | ||
Check failure on line 111 in src/components/learner-credit-management/BudgetCard-V2.jsx GitHub Actions / tests
|
||
{renderCardHeader('Overview', '256829')} | ||
{renderCardSection(offerSummary?.remainingFunds, offerSummary?.redeemedFunds)} | ||
</Stack> | ||
</Card.Body> | ||
</Card> | ||
</> | ||
</Stack> | ||
); | ||
}; | ||
|
@@ -161,7 +128,7 @@ | |
}).isRequired, | ||
enterpriseUUID: PropTypes.string.isRequired, | ||
enterpriseSlug: PropTypes.string.isRequired, | ||
enableLearnerPortal: PropTypes.bool.isRequired, | ||
}; | ||
|
||
export default BudgetCard; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import React, { useContext } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { | ||
Row, | ||
Col, | ||
Breadcrumb, | ||
} from '@edx/paragon'; | ||
import { connect } from 'react-redux'; | ||
import { Helmet } from 'react-helmet'; | ||
import { useParams } from 'react-router-dom'; | ||
import Hero from '../Hero'; | ||
|
||
import LoadingMessage from '../LoadingMessage'; | ||
import { EnterpriseSubsidiesContext } from '../EnterpriseSubsidiesContext'; | ||
|
||
import LearnerCreditAllocationTable from './LearnerCreditAllocationTable'; | ||
import { useOfferRedemptions } from './data/hooks'; | ||
import { isUUID } from './data/utils'; | ||
import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; | ||
|
||
const PAGE_TITLE = 'Learner Credit Budget Detail'; | ||
|
||
const BudgetDetailPage = ({ | ||
enterpriseUUID, | ||
enterpriseSlug, | ||
}) => { | ||
const { id } = useParams(); | ||
const offerId = isUUID(id) ? null : id; | ||
const budgetId = isUUID(id) ? id : null; | ||
|
||
const { isLoading } = useContext(EnterpriseSubsidiesContext); | ||
const { | ||
isLoading: isLoadingOfferRedemptions, | ||
offerRedemptions, | ||
fetchOfferRedemptions, | ||
} = useOfferRedemptions(enterpriseUUID, offerId, budgetId); | ||
if (isLoading) { | ||
return <LoadingMessage className="offers" />; | ||
} | ||
const links = [ | ||
{ label: 'Budgets', href: `/${enterpriseSlug}/admin/${ROUTE_NAMES.learnerCredit}` }, | ||
]; | ||
return ( | ||
<> | ||
<Helmet title={PAGE_TITLE} /> | ||
<Hero title={PAGE_TITLE} /> | ||
<Row className="m-3"> | ||
<Col xs="12"> | ||
<Breadcrumb | ||
ariaLabel="Breadcrumb" | ||
links={links} | ||
activeLabel="Overview" | ||
/> | ||
</Col> | ||
</Row> | ||
<LearnerCreditAllocationTable | ||
isLoading={isLoadingOfferRedemptions} | ||
tableData={offerRedemptions} | ||
fetchTableData={fetchOfferRedemptions} | ||
enterpriseUUID={enterpriseUUID} | ||
enterpriseSlug={enterpriseSlug} | ||
enableLearnerPortal | ||
/> | ||
</> | ||
); | ||
}; | ||
|
||
const mapStateToProps = state => ({ | ||
enterpriseUUID: state.portalConfiguration.enterpriseId, | ||
enterpriseSlug: state.portalConfiguration.enterpriseSlug, | ||
}); | ||
|
||
BudgetDetailPage.propTypes = { | ||
enterpriseUUID: PropTypes.string.isRequired, | ||
enterpriseSlug: PropTypes.string.isRequired, | ||
}; | ||
|
||
export default connect(mapStateToProps)(BudgetDetailPage); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
/* eslint-disable react/prop-types */ | ||
import React from 'react'; | ||
import { Provider } from 'react-redux'; | ||
import thunk from 'redux-thunk'; | ||
import userEvent from '@testing-library/user-event'; | ||
import configureMockStore from 'redux-mock-store'; | ||
import { | ||
screen, | ||
render, | ||
waitFor, | ||
} from '@testing-library/react'; | ||
import '@testing-library/jest-dom/extend-expect'; | ||
|
||
import { IntlProvider } from '@edx/frontend-platform/i18n'; | ||
import BudgetDetailPage from '../BudgetDetailPage'; | ||
import { useOfferSummary, useOfferRedemptions } from '../data/hooks'; | ||
import { EXEC_ED_OFFER_TYPE } from '../data/constants'; | ||
import { MemoryRouter } from 'react-router-dom'; | ||
|
||
jest.mock('../data/hooks'); | ||
useOfferSummary.mockReturnValue({ | ||
isLoading: false, | ||
offerSummary: null, | ||
}); | ||
useOfferRedemptions.mockReturnValue({ | ||
isLoading: false, | ||
offerRedemptions: { | ||
itemCount: 0, | ||
pageCount: 0, | ||
results: [], | ||
}, | ||
fetchOfferRedemptions: jest.fn(), | ||
}); | ||
|
||
const mockStore = configureMockStore([thunk]); | ||
const getMockStore = store => mockStore(store); | ||
const enterpriseId = 'test-enterprise'; | ||
const enterpriseUUID = '1234'; | ||
const initialStore = { | ||
portalConfiguration: { | ||
enterpriseId, | ||
enterpriseSlug:enterpriseId | ||
|
||
}, | ||
}; | ||
const store = getMockStore({ ...initialStore }); | ||
|
||
const mockEnterpriseOfferId = '123'; | ||
const mockEnterpriseOfferEnrollmentId = 456; | ||
|
||
const mockOfferDisplayName = 'Test Enterprise Offer'; | ||
const mockOfferSummary = { | ||
totalFunds: 5000, | ||
redeemedFunds: 200, | ||
remainingFunds: 4800, | ||
percentUtilized: 0.04, | ||
offerType: EXEC_ED_OFFER_TYPE, | ||
}; | ||
|
||
const BudgetDetailPageWrapper = ({ ...rest }) => ( | ||
<MemoryRouter initialEntries={['/test-enterprise/admin/learner-credit/1234']}> | ||
|
||
<Provider store={store}> | ||
<IntlProvider locale="en"> | ||
<BudgetDetailPage {...rest} /> | ||
</IntlProvider> | ||
</Provider> | ||
</MemoryRouter> | ||
); | ||
|
||
describe('<BudgetDetailPage />', () => { | ||
describe('with enterprise offer', () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('displays table on clicking view budget', async () => { | ||
const mockOffer = { | ||
id: mockEnterpriseOfferId, | ||
name: mockOfferDisplayName, | ||
start: '2022-01-01', | ||
end: '2023-01-01', | ||
}; | ||
useOfferSummary.mockReturnValue({ | ||
isLoading: false, | ||
offerSummary: mockOfferSummary, | ||
}); | ||
useOfferRedemptions.mockReturnValue({ | ||
isLoading: false, | ||
offerRedemptions: { | ||
itemCount: 0, | ||
pageCount: 0, | ||
results: [], | ||
}, | ||
fetchOfferRedemptions: jest.fn(), | ||
}); | ||
render(<BudgetDetailPageWrapper | ||
enterpriseUUID={enterpriseUUID} enterpriseSlug={enterpriseId} | ||
offer={mockOffer} | ||
/>); | ||
expect(screen.getByText('Learner Credit Budget Detail')); | ||
expect(screen.getByText('Overview')); | ||
expect(screen.getByText('No results found')); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[suggestion] Given this
Route
and the aboveRoute
both relate to the same Learner Credit Management (LCM) feature, I'm wondering if this component should be defining a single parent route for the overall LCM feature, and then the component passed to the parent route has its owned rendered sub-routes for the LCM-specific routes. That way,EnterpriseAppRoutes
, doesn't really need any knowledge of the detail route.Related, as is, this LCM detail page route would be available even when the LCM overview page route (defined above) is not since it isn't relying on the
canManageLearnerCredit
variable. If the overview page route isn't available; neither should the detail page route.