Skip to content
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

feat: ENT-7309 Added budget category based page for learner credit #1003

Merged
merged 7 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions src/components/learner-credit-management/BudgetCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import dayjs from 'dayjs';
import {
Card,
Button,
Stack,
Row,
Col,
Breadcrumb,
} from '@edx/paragon';

import { getCourseProductLineAbbreviation } from '../../utils';
import { useOfferRedemptions, useOfferSummary } from './data/hooks';
import LearnerCreditAggregateCards from './LearnerCreditAggregateCards';
import LearnerCreditAllocationTable from './LearnerCreditAllocationTable';
import { ROUTE_NAMES } from '../EnterpriseApp/data/constants';

const BudgetCard = ({
offer,
enterpriseUUID,
enterpriseSlug,
}) => {
const {
start,
end,
} = offer;

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 renderActions = (budgetType) => (
<Button
data-testid="view-budget"
onClick={() => navigateToBudgetRedemptions(budgetType)}
>
View Budget
</Button>
);

const renderCardHeader = (budgetType) => {
const subtitle = (
<div className="d-flex flex-wrap align-items-center">
<span data-testid="offer-date">
{formattedStartDate} - {formattedExpirationDate}
</span>
</div>
);

return (
<Card.Header
title={budgetType}
subtitle={subtitle}
actions={(
<div>
{renderActions(budgetType)}
</div>
)}
/>
);
};

const renderCardSection = (available, spent) => (
<Card.Section
title="Balance"
muted
>
<Row className="d-flex flex-row justify-content-start w-md-75">
<Col xs="6" md="auto" className="d-flex flex-column mb-3 mb-md-0">
<span className="small">Available</span>
<span>{available}</span>
</Col>
<Col xs="6" md="auto" className="d-flex flex-column mb-3 mb-md-0">
<span className="small">Spent</span>
<span>{spent}</span>
</Col>
</Row>
</Card.Section>
);

const renderCardAggregate = () => (
<div className="mb-4.5 d-flex flex-wrap mx-n3">
<LearnerCreditAggregateCards
isLoading={isLoadingOfferSummary}
totalFunds={offerSummary?.totalFunds}
redeemedFunds={offerSummary?.redeemedFunds}
remainingFunds={offerSummary?.remainingFunds}
percentUtilized={offerSummary?.percentUtilized}
/>
</div>
);

return (
<Stack>
<Row className="m-3">
<Col xs="12">
<Breadcrumb
ariaLabel="Breadcrumb is active"
links={links}
activeLabel={activeLabel}
/>
</Col>
</Row>
{!detailPage
? (
<>
{renderCardAggregate()}
<h2>Budgets</h2>
<Card
orientation="horizontal"
>
<Card.Body>
<Stack gap={4}>
{renderCardHeader('Open Courses Marketplace')}
{renderCardSection(offerSummary?.remainingFunds, offerSummary?.redeemedFundsOcm)}
</Stack>
</Card.Body>
</Card>
<Card
orientation="horizontal"
>
<Card.Body>
<Stack gap={4}>
{renderCardHeader('Executive Education')}
{renderCardSection(offerSummary?.remainingFunds, offerSummary?.redeemedFundsExecEd)}
</Stack>
</Card.Body>
</Card>
</>
)
: (
<LearnerCreditAllocationTable
isLoading={isLoadingOfferRedemptions}
tableData={offerRedemptions}
fetchTableData={fetchOfferRedemptions}
enterpriseUUID={enterpriseUUID}
budgetType={getCourseProductLineAbbreviation(activeLabel)}
/>
)}
</Stack>
);
};

BudgetCard.propTypes = {
offer: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
start: PropTypes.string.isRequired,
end: PropTypes.string.isRequired,
}).isRequired,
enterpriseUUID: PropTypes.string.isRequired,
enterpriseSlug: PropTypes.string.isRequired,
};

export default BudgetCard;
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ const LearnerCreditAllocationTable = ({
tableData,
fetchTableData,
enterpriseUUID,
budgetType,
}) => {
const isDesktopTable = useMediaQuery({ minWidth: breakpoints.extraLarge.minWidth });
const defaultFilter = budgetType ? [{ id: 'courseProductLine', value: budgetType }] : [];

return (
<DataTable
isSortable
Expand Down Expand Up @@ -57,6 +60,7 @@ const LearnerCreditAllocationTable = ({
Header: 'Product',
accessor: 'courseProductLine',
Cell: ({ row }) => getCourseProductLineText(row.values.courseProductLine),
disableFilters: true,
},
]}
initialTableOptions={{
Expand All @@ -68,6 +72,7 @@ const LearnerCreditAllocationTable = ({
sortBy: [
{ id: 'enrollmentDate', desc: true },
],
filters: defaultFilter,
}}
fetchData={fetchTableData}
data={tableData.results}
Expand All @@ -85,6 +90,9 @@ const LearnerCreditAllocationTable = ({
/>
);
};
LearnerCreditAllocationTable.defaultProps = {
budgetType: null,
};

LearnerCreditAllocationTable.propTypes = {
enterpriseUUID: PropTypes.string.isRequired,
Expand All @@ -101,6 +109,7 @@ LearnerCreditAllocationTable.propTypes = {
pageCount: PropTypes.number.isRequired,
}).isRequired,
fetchTableData: PropTypes.func.isRequired,
budgetType: PropTypes.string,
};

export default LearnerCreditAllocationTable;
84 changes: 84 additions & 0 deletions src/components/learner-credit-management/MultipleBudgetsPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import {
Stack,
Row,
Col,
Card,
Hyperlink,
} from '@edx/paragon';
import { connect } from 'react-redux';
import { Helmet } from 'react-helmet';
import Hero from '../Hero';

import LoadingMessage from '../LoadingMessage';
import MultipleBudgetsPicker from './MultipleBudgetsPicker';
import { EnterpriseSubsidiesContext } from '../EnterpriseSubsidiesContext';

import { configuration } from '../../config';

const PAGE_TITLE = 'Learner Credit';

const MultipleBudgetsPage = ({
enterpriseUUID,
enterpriseSlug,
}) => {
const { offers, isLoading } = useContext(EnterpriseSubsidiesContext);

if (isLoading) {
return <LoadingMessage className="offers" />;

Check warning on line 29 in src/components/learner-credit-management/MultipleBudgetsPage.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/learner-credit-management/MultipleBudgetsPage.jsx#L29

Added line #L29 was not covered by tests
}

if (offers.length === 0) {
return (
<Stack>
<Helmet title={PAGE_TITLE} />
<Hero title={PAGE_TITLE} />
<Card>
<Card.Section className="text-center">
<Row>
<Col xs={12} lg={{ span: 8, offset: 2 }}>
<h3 className="mb-3">No budgets for your organization</h3>
<p>
We were unable to find any budgets for your organization. Please contact
Customer Support if you have questions.
</p>
<Hyperlink
className="btn btn-brand"
target="_blank"
destination={configuration.ENTERPRISE_SUPPORT_URL}
>
Contact support
</Hyperlink>
</Col>
</Row>
</Card.Section>
</Card>
</Stack>
);
}

return (

Check warning on line 61 in src/components/learner-credit-management/MultipleBudgetsPage.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/learner-credit-management/MultipleBudgetsPage.jsx#L61

Added line #L61 was not covered by tests
<>
<Helmet title={PAGE_TITLE} />
<Hero title={PAGE_TITLE} />
<MultipleBudgetsPicker
offers={offers}
enterpriseUUID={enterpriseUUID}
enterpriseSlug={enterpriseSlug}
/>
</>
);
};

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

MultipleBudgetsPage.propTypes = {
enterpriseUUID: PropTypes.string.isRequired,
enterpriseSlug: PropTypes.string.isRequired,
};

export default connect(mapStateToProps)(MultipleBudgetsPage);
38 changes: 38 additions & 0 deletions src/components/learner-credit-management/MultipleBudgetsPicker.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Stack,
Row,
Col,
} from '@edx/paragon';

import BudgetCard from './BudgetCard';

const MultipleBudgetsPicker = ({
offers,
enterpriseUUID,
enterpriseSlug,
}) => (
<Stack>

Check warning on line 16 in src/components/learner-credit-management/MultipleBudgetsPicker.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/learner-credit-management/MultipleBudgetsPicker.jsx#L16

Added line #L16 was not covered by tests
<Row>
<Col lg="10">
{offers.map(offer => (
<BudgetCard

Check warning on line 20 in src/components/learner-credit-management/MultipleBudgetsPicker.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/learner-credit-management/MultipleBudgetsPicker.jsx#L20

Added line #L20 was not covered by tests
key={offer.id}
offer={offer}
enterpriseUUID={enterpriseUUID}
enterpriseSlug={enterpriseSlug}
/>
))}
</Col>
</Row>
</Stack>
);

MultipleBudgetsPicker.propTypes = {
offers: PropTypes.arrayOf(PropTypes.shape()).isRequired,
enterpriseUUID: PropTypes.string.isRequired,
enterpriseSlug: PropTypes.string.isRequired,
};

export default MultipleBudgetsPicker;
4 changes: 4 additions & 0 deletions src/components/learner-credit-management/data/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,16 @@
const applyFiltersToOptions = (filters, options) => {
const userSearchQuery = filters?.find(filter => filter.id === 'userEmail')?.value;
const courseTitleSearchQuery = filters?.find(filter => filter.id === 'courseTitle')?.value;
const courseProductLineSearchQuery = filters?.find(filter => filter.id === 'courseProductLine')?.value;
if (userSearchQuery) {
Object.assign(options, { search: userSearchQuery });
}
if (courseTitleSearchQuery) {
Object.assign(options, { searchCourse: courseTitleSearchQuery });
}
if (courseProductLineSearchQuery) {
Object.assign(options, { courseProductLine: courseProductLineSearchQuery });

Check warning on line 76 in src/components/learner-credit-management/data/hooks.js

View check run for this annotation

Codecov / codecov/patch

src/components/learner-credit-management/data/hooks.js#L76

Added line #L76 was not covered by tests
}
};

export const useOfferRedemptions = (enterpriseUUID, offerId) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ describe('useOfferSummary', () => {
const expectedResult = {
totalFunds: 5000,
redeemedFunds: 200,
redeemedFundsExecEd: NaN,
redeemedFundsOcm: NaN,
remainingFunds: 4800,
percentUtilized: 0.04,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ describe('transformOfferSummary', () => {
expect(transformOfferSummary(offerSummary)).toEqual({
totalFunds: 1,
redeemedFunds: 1,
redeemedFundsExecEd: NaN,
redeemedFundsOcm: NaN,
remainingFunds: 0.0,
percentUtilized: 1.0,
});
Expand Down
Loading