Skip to content

Commit

Permalink
feat: ENT-7309 Added budget category based page for learner credit (#…
Browse files Browse the repository at this point in the history
…1003)

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

Co-authored-by: IrfanUddinAhmad <[email protected]>
Co-authored-by: zamanafzal <[email protected]>
  • Loading branch information
3 people authored Aug 3, 2023
1 parent 21489d0 commit 8500f4d
Show file tree
Hide file tree
Showing 13 changed files with 535 additions and 2 deletions.
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" />;
}

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 (
<>
<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>
<Row>
<Col lg="10">
{offers.map(offer => (
<BudgetCard
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 applySortByToOptions = (sortBy, options) => {
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 });
}
};

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

0 comments on commit 8500f4d

Please sign in to comment.