From 27abfcc4a9ac3acc85f0916a424aa67c31714f66 Mon Sep 17 00:00:00 2001 From: jajjibhai008 Date: Fri, 15 Sep 2023 19:03:16 +0500 Subject: [PATCH 1/2] feat: show all enterprise budgets regardless of plan and route correctly fix: full page refresh issue when clicking 'Budgets', added test coverage and fixed lint issues --- .../EnterpriseApp/EnterpriseAppRoutes.jsx | 7 + .../EnterpriseSubsidiesContext/data/hooks.js | 71 ++++---- .../data/tests/BudgetDetailPage.test.jsx | 115 +++++++++++++ .../data/tests/hooks.test.js | 106 +++++++----- .../BudgetCard-V2.jsx | 157 ++++-------------- .../BudgetDetailPage.jsx | 79 +++++++++ .../Budgetcard-V3.jsx | 99 +++++++++++ .../MultipleBudgetsPicker.jsx | 1 + .../learner-credit-management/data/hooks.js | 13 +- .../data/tests/hooks.test.js | 3 + .../data/tests/utils.test.js | 8 + .../learner-credit-management/data/utils.js | 25 ++- .../tests/BudgetCard.test.jsx | 65 ++------ 13 files changed, 505 insertions(+), 244 deletions(-) create mode 100644 src/components/EnterpriseSubsidiesContext/data/tests/BudgetDetailPage.test.jsx create mode 100644 src/components/learner-credit-management/BudgetDetailPage.jsx create mode 100644 src/components/learner-credit-management/Budgetcard-V3.jsx diff --git a/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx b/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx index b5853281d5..f6d1bf1381 100644 --- a/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx +++ b/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx @@ -14,6 +14,7 @@ import { PlotlyAnalyticsPage } from '../PlotlyAnalytics'; 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,12 @@ const EnterpriseAppRoutes = ({ /> )} + + {enableContentHighlightsPage && ( mockStore(store); +const enterpriseId = 'test-enterprise'; +const enterpriseUUID = '1234'; +const initialStore = { + portalConfiguration: { + enterpriseId, + enterpriseSlug: enterpriseId, + + }, +}; +const store = getMockStore({ ...initialStore }); + +const mockEnterpriseOfferId = '123'; + +const mockOfferDisplayName = 'Test Enterprise Offer'; +const mockOfferSummary = { + totalFunds: 5000, + redeemedFunds: 200, + remainingFunds: 4800, + percentUtilized: 0.04, + offerType: EXEC_ED_OFFER_TYPE, +}; + +const defaultEnterpriseSubsidiesContextValue = { + isLoading: false, +}; + +const BudgetDetailPageWrapper = ({ + enterpriseSubsidiesContextValue = defaultEnterpriseSubsidiesContextValue, + ...rest +}) => ( + + + + + + + + + + +); + +describe('', () => { + 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(); + expect(screen.getByText('Learner Credit Budget Detail')); + expect(screen.getByText('Overview')); + expect(screen.getByText('No results found')); + }); + }); +}); diff --git a/src/components/EnterpriseSubsidiesContext/data/tests/hooks.test.js b/src/components/EnterpriseSubsidiesContext/data/tests/hooks.test.js index adf8580b52..2a4fe3496c 100644 --- a/src/components/EnterpriseSubsidiesContext/data/tests/hooks.test.js +++ b/src/components/EnterpriseSubsidiesContext/data/tests/hooks.test.js @@ -51,6 +51,7 @@ describe('useEnterpriseOffers', () => { start: '2021-05-15T19:56:09Z', end: '2100-05-15T19:56:09Z', isCurrent: true, + source: 'ecommerceApi', }]; SubsidyApiService.getSubsidyByCustomerUUID.mockResolvedValueOnce({ @@ -80,25 +81,35 @@ describe('useEnterpriseOffers', () => { }); it('should fetch enterprise offers for the enterprise when data available in enterprise-subsidy', async () => { - const mockOffers = [ + const mockEnterpriseSubsidyResponse = [ { - id: 'offer-id', - name: 'offer-name', - start: '2021-05-15T19:56:09Z', - end: '2100-05-15T19:56:09Z', - isCurrent: true, + uuid: 'offer-id', + title: 'offer-name', + activeDatetime: '2021-05-15T19:56:09Z', + expirationDatetime: '2100-05-15T19:56:09Z', + isActive: true, }, ]; - const mockSubsidyServiceResponse = [{ - uuid: 'offer-id', - title: 'offer-name', - active_datetime: '2021-05-15T19:56:09Z', - expiration_datetime: '2100-05-15T19:56:09Z', - is_active: true, - }]; + + const mockEcommerceResponse = [ + { + id: 'uuid', + display_name: 'offer-name', + start_datetime: '2021-05-15T19:56:09Z', + end_datetime: '2100-05-15T19:56:09Z', + is_current: true, + }, + ]; + SubsidyApiService.getSubsidyByCustomerUUID.mockResolvedValueOnce({ data: { - results: mockSubsidyServiceResponse, + results: mockEnterpriseSubsidyResponse, + }, + }); + + EcommerceApiService.fetchEnterpriseOffers.mockResolvedValueOnce({ + data: { + results: mockEcommerceResponse, }, }); @@ -113,36 +124,39 @@ describe('useEnterpriseOffers', () => { TEST_ENTERPRISE_UUID, { subsidyType: 'learner_credit' }, ); + + const expectedOffers = [ + { + id: 'offer-id', + name: 'offer-name', + start: '2021-05-15T19:56:09Z', + end: '2100-05-15T19:56:09Z', + isCurrent: true, + source: 'subsidyApi', + }, + { + id: 'uuid', + name: 'offer-name', + start: '2021-05-15T19:56:09Z', + end: '2100-05-15T19:56:09Z', + isCurrent: true, + source: 'ecommerceApi', + }, + ]; + expect(result.current).toEqual({ - offers: mockOffers, + offers: expectedOffers, isLoading: false, canManageLearnerCredit: true, }); }); it('should set canManageLearnerCredit to false if active enterprise offer or subsidy not found', async () => { - const mockOffers = [{ subsidyUuid: 'offer-1' }, { subsidyUuid: 'offer-2' }]; - const mockSubsidyServiceResponse = [ - { - uuid: 'offer-1', - title: 'offer-name', - active_datetime: '2005-05-15T19:56:09Z', - expiration_datetime: '2006-05-15T19:56:09Z', - is_active: false, - }, - { - uuid: 'offer-2', - title: 'offer-name-2', - active_datetime: '2006-05-15T19:56:09Z', - expiration_datetime: '2007-05-15T19:56:09Z', - is_active: false, - }, - ]; - const mockOfferData = []; + const mockSubsidyServiceResponse = []; EcommerceApiService.fetchEnterpriseOffers.mockResolvedValueOnce({ data: { - results: mockOffers, + results: [], }, }); SubsidyApiService.getSubsidyByCustomerUUID.mockResolvedValueOnce({ @@ -162,15 +176,22 @@ describe('useEnterpriseOffers', () => { TEST_ENTERPRISE_UUID, { subsidyType: 'learner_credit' }, ); + + const hasActiveOffersOrSubsidies = mockSubsidyServiceResponse.some(offer => offer.is_active); + let canManageLearnerCredit = false; + + if (hasActiveOffersOrSubsidies) { + canManageLearnerCredit = true; + } + expect(result.current).toEqual({ - offers: mockOfferData, + offers: [], isLoading: false, - canManageLearnerCredit: false, + canManageLearnerCredit, }); }); it('should return the active enterprise offer or subsidy when multiple available', async () => { - const mockOffers = [{ subsidyUuid: 'offer-1' }, { subsidyUuid: 'offer-2' }]; const mockSubsidyServiceResponse = [ { uuid: 'offer-1', @@ -188,18 +209,27 @@ describe('useEnterpriseOffers', () => { }, ]; const mockOfferData = [ + { + id: 'offer-1', + name: 'offer-name', + start: '2005-05-15T19:56:09Z', + end: '2006-05-15T19:56:09Z', + isCurrent: false, + source: 'subsidyApi', + }, { id: 'offer-2', name: 'offer-name-2', start: '2006-05-15T19:56:09Z', end: '2099-05-15T19:56:09Z', isCurrent: true, + source: 'subsidyApi', }, ]; EcommerceApiService.fetchEnterpriseOffers.mockResolvedValueOnce({ data: { - results: mockOffers, + results: [], }, }); SubsidyApiService.getSubsidyByCustomerUUID.mockResolvedValueOnce({ diff --git a/src/components/learner-credit-management/BudgetCard-V2.jsx b/src/components/learner-credit-management/BudgetCard-V2.jsx index b39b9297d9..09a5c8cdf5 100644 --- a/src/components/learner-credit-management/BudgetCard-V2.jsx +++ b/src/components/learner-credit-management/BudgetCard-V2.jsx @@ -1,25 +1,18 @@ -import React, { useState } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import dayjs from 'dayjs'; import { - Card, - Button, Stack, - Row, - Col, - Breadcrumb, } from '@edx/paragon'; -import { useOfferRedemptions, useOfferSummary } from './data/hooks'; -import LearnerCreditAggregateCards from './LearnerCreditAggregateCards'; -import LearnerCreditAllocationTable from './LearnerCreditAllocationTable'; -import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; +import { useOfferSummary } from './data/hooks'; +import SubBudgetCard from './Budgetcard-V3'; const BudgetCard = ({ offer, enterpriseUUID, enterpriseSlug, - enableLearnerPortal, + offerType, }) => { const { start, @@ -31,123 +24,37 @@ const BudgetCard = ({ 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) => ( - - ); - - const renderCardHeader = (budgetType) => { - const subtitle = ( -
- - {formattedStartDate} - {formattedExpirationDate} - -
- ); - - return ( - - {renderActions(budgetType)} - - )} - /> - ); - }; - - const renderCardSection = (available, spent) => ( - - - - Available - {available} - - - Spent - {spent} - - - - ); - - const renderCardAggregate = () => ( -
- -
- ); return ( - - - - - - - {!detailPage - ? ( - <> - {renderCardAggregate()} -

Budgets

- - - - {renderCardHeader('Overview')} - {renderCardSection(offerSummary?.remainingFunds, offerSummary?.redeemedFunds)} - - - - - ) - : ( - - )} + + <> +

Budgets

+ {!isLoadingOfferSummary + && offerType === 'ecommerceApi' + ? ( + + ) + : offerSummary?.budgetsSumary.map((budget) => ( + + ))} +
); }; @@ -161,7 +68,7 @@ BudgetCard.propTypes = { }).isRequired, enterpriseUUID: PropTypes.string.isRequired, enterpriseSlug: PropTypes.string.isRequired, - enableLearnerPortal: PropTypes.bool.isRequired, + offerType: PropTypes.string.isRequired, }; export default BudgetCard; diff --git a/src/components/learner-credit-management/BudgetDetailPage.jsx b/src/components/learner-credit-management/BudgetDetailPage.jsx new file mode 100644 index 0000000000..1a97c72363 --- /dev/null +++ b/src/components/learner-credit-management/BudgetDetailPage.jsx @@ -0,0 +1,79 @@ +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, Link } 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 ; + } + const links = [ + { label: 'Budgets', to: `/${enterpriseSlug}/admin/${ROUTE_NAMES.learnerCredit}` }, + ]; + return ( + <> + + + + + + + + + + ); +}; + +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); diff --git a/src/components/learner-credit-management/Budgetcard-V3.jsx b/src/components/learner-credit-management/Budgetcard-V3.jsx new file mode 100644 index 0000000000..97c275b4e7 --- /dev/null +++ b/src/components/learner-credit-management/Budgetcard-V3.jsx @@ -0,0 +1,99 @@ +import { Link } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import dayjs from 'dayjs'; +import { + Card, + Button, + Stack, + Row, + Col, +} from '@edx/paragon'; + +import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; + +const SubBudgetCard = ({ + id, + start, + end, + available, + spent, + enterpriseSlug, +}) => { + const formattedStartDate = dayjs(start).format('MMMM D, YYYY'); + const formattedExpirationDate = dayjs(end).format('MMMM D, YYYY'); + + const renderActions = (id) => ( + + ); + + const renderCardHeader = (budgetType, id) => { + const subtitle = ( +
+ + {formattedStartDate} - {formattedExpirationDate} + +
+ ); + + return ( + + {renderActions(id)} + + )} + /> + ); + }; + + const renderCardSection = (available, spent) => ( + + + + Available + {available} + + + Spent + {spent} + + + + ); + + return ( + + + + {renderCardHeader('Overview', id)} + {renderCardSection(available, spent)} + + + + ); +}; + +SubBudgetCard.propTypes = { + enterpriseSlug: PropTypes.string.isRequired, + id: PropTypes.string, + start: PropTypes.string, + end: PropTypes.string, + spent: PropTypes.number, + available: PropTypes.number, + +}; + +export default SubBudgetCard; diff --git a/src/components/learner-credit-management/MultipleBudgetsPicker.jsx b/src/components/learner-credit-management/MultipleBudgetsPicker.jsx index 4c3da2d0ce..9dc6a227c1 100644 --- a/src/components/learner-credit-management/MultipleBudgetsPicker.jsx +++ b/src/components/learner-credit-management/MultipleBudgetsPicker.jsx @@ -24,6 +24,7 @@ const MultipleBudgetsPicker = ({ enterpriseUUID={enterpriseUUID} enterpriseSlug={enterpriseSlug} enableLearnerPortal={enableLearnerPortal} + offerType={offer.source} /> ))} diff --git a/src/components/learner-credit-management/data/hooks.js b/src/components/learner-credit-management/data/hooks.js index 585970c35e..5e107e9428 100644 --- a/src/components/learner-credit-management/data/hooks.js +++ b/src/components/learner-credit-management/data/hooks.js @@ -74,7 +74,7 @@ const applyFiltersToOptions = (filters, options) => { } }; -export const useOfferRedemptions = (enterpriseUUID, offerId) => { +export const useOfferRedemptions = (enterpriseUUID, offerId= null, budgetId = null) => { const shouldTrackFetchEvents = useRef(false); const [isLoading, setIsLoading] = useState(true); const [offerRedemptions, setOfferRedemptions] = useState({ @@ -90,9 +90,14 @@ export const useOfferRedemptions = (enterpriseUUID, offerId) => { const options = { page: args.pageIndex + 1, // `DataTable` uses zero-indexed array pageSize: args.pageSize, - offerId, ignoreNullCourseListPrice: true, }; + if (budgetId !== null) { + options.budgetId = budgetId; + } + if (offerId !== null) { + options.offerId = offerId; + } if (args.sortBy?.length > 0) { applySortByToOptions(args.sortBy, options); } @@ -129,10 +134,10 @@ export const useOfferRedemptions = (enterpriseUUID, offerId) => { setIsLoading(false); } }; - if (offerId) { + if (offerId || budgetId) { fetch(); } - }, [enterpriseUUID, offerId, shouldTrackFetchEvents]); + }, [enterpriseUUID, offerId, budgetId, shouldTrackFetchEvents]); const debouncedFetchOfferRedemptions = useMemo(() => debounce(fetchOfferRedemptions, 300), [fetchOfferRedemptions]); diff --git a/src/components/learner-credit-management/data/tests/hooks.test.js b/src/components/learner-credit-management/data/tests/hooks.test.js index 8ab61bce2f..f70f533d4d 100644 --- a/src/components/learner-credit-management/data/tests/hooks.test.js +++ b/src/components/learner-credit-management/data/tests/hooks.test.js @@ -72,6 +72,9 @@ describe('useOfferSummary', () => { redeemedFundsOcm: NaN, remainingFunds: 4800, percentUtilized: 0.04, + offerId: 1, + budgetsSumary: [], + offerType: undefined, }; expect(result.current).toEqual({ offerSummary: expectedResult, diff --git a/src/components/learner-credit-management/data/tests/utils.test.js b/src/components/learner-credit-management/data/tests/utils.test.js index 33902d40fe..b24abe27d4 100644 --- a/src/components/learner-credit-management/data/tests/utils.test.js +++ b/src/components/learner-credit-management/data/tests/utils.test.js @@ -23,6 +23,8 @@ describe('transformOfferSummary', () => { remainingFunds: 0.0, percentUtilized: 1.0, offerType: EXEC_ED_OFFER_TYPE, + budgetsSumary: [], + offerId: undefined, }); }); @@ -33,6 +35,8 @@ describe('transformOfferSummary', () => { remainingBalance: null, percentOfOfferSpent: null, offerType: 'Site', + offerId: '123', + budgetsSumary: [], }; expect(transformOfferSummary(offerSummary)).toEqual({ @@ -41,6 +45,10 @@ describe('transformOfferSummary', () => { remainingFunds: null, percentUtilized: null, offerType: 'Site', + redeemedFundsExecEd: undefined, + redeemedFundsOcm: undefined, + offerId: '123', + budgetsSumary: [], }); }); }); diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 65524c1346..1c7a52c459 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -12,7 +12,23 @@ import { */ export const transformOfferSummary = (offerSummary) => { if (!offerSummary) { return null; } + let budgetsSumary = [] + if (offerSummary?.budgets) { + const budgets = offerSummary?.budgets + for (let i = 0; i < budgets.length; i++) { + let redeemedFunds = budgets[i].amountOfPolicySpent && parseFloat(budgets[i].amountOfPolicySpent); + let remainingFunds = budgets[i].remainingBalance && parseFloat(budgets[i].remainingBalance); + // Create an object with key-value pairs + const budgetEntry = { + redeemedFunds, + remainingFunds, + ...budgets[i] + }; + budgetsSumary.push(budgetEntry); + } + } + const totalFunds = offerSummary.maxDiscount && parseFloat(offerSummary.maxDiscount); let redeemedFunds = offerSummary.amountOfOfferSpent && parseFloat(offerSummary.amountOfOfferSpent); let redeemedFundsOcm = offerSummary.amountOfferSpentOcm && parseFloat(offerSummary.amountOfferSpentOcm); @@ -38,7 +54,7 @@ export const transformOfferSummary = (offerSummary) => { percentUtilized = Math.min(percentUtilized, 1.0); } const { offerType } = offerSummary; - + const { offerId } = offerSummary; return { totalFunds, redeemedFunds, @@ -47,6 +63,8 @@ export const transformOfferSummary = (offerSummary) => { remainingFunds, percentUtilized, offerType, + offerId, + budgetsSumary, }; }; @@ -91,3 +109,8 @@ export const getProgressBarVariant = ({ percentUtilized, remainingFunds }) => { } return variant; }; + +// Utility function to check if the ID is a UUID +export const isUUID = (id) => { + return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(id); +} diff --git a/src/components/learner-credit-management/tests/BudgetCard.test.jsx b/src/components/learner-credit-management/tests/BudgetCard.test.jsx index 7d8f349bda..dcff64ce3e 100644 --- a/src/components/learner-credit-management/tests/BudgetCard.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetCard.test.jsx @@ -1,21 +1,19 @@ /* eslint-disable react/prop-types */ import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; -import userEvent from '@testing-library/user-event'; import configureMockStore from 'redux-mock-store'; import dayjs from 'dayjs'; import { screen, render, - waitFor, } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import BudgetCard from '../BudgetCard-V2'; import { useOfferSummary, useOfferRedemptions } from '../data/hooks'; -import { EXEC_ED_OFFER_TYPE } from '../data/constants'; jest.mock('../data/hooks'); useOfferSummary.mockReturnValue({ @@ -47,20 +45,15 @@ 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 BudgetCardWrapper = ({ ...rest }) => ( - - - - - + + + + + + + ); describe('', () => { @@ -88,6 +81,16 @@ describe('', () => { remainingFunds: 4800, percentUtilized: 0.04, offerType: 'Site', + budgetsSumary: [ + { + id: 123, + start: '2022-01-01', + end: '2022-01-01', + available: 200, + spent: 100, + enterpriseSlug: enterpriseId, + }, + ], }, }); useOfferRedemptions.mockReturnValue({ @@ -106,42 +109,10 @@ describe('', () => { />); expect(screen.getByText('Overview')); expect(screen.queryByText('Executive Education')).not.toBeInTheDocument(); - expect(screen.getByText(`$${mockOfferSummary.redeemedFunds.toLocaleString()}`)); const formattedString = `${dayjs(mockOffer.start).format('MMMM D, YYYY')} - ${dayjs(mockOffer.end).format('MMMM D, YYYY')}`; const elementsWithTestId = screen.getAllByTestId('offer-date'); const firstElementWithTestId = elementsWithTestId[0]; expect(firstElementWithTestId).toHaveTextContent(formattedString); }); - - 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(); - const elementsWithTestId = screen.getAllByTestId('view-budget'); - const firstElementWithTestId = elementsWithTestId[0]; - await waitFor(() => userEvent.click(firstElementWithTestId)); - expect(screen.getByText('No results found')); - }); }); }); From 0ec47bad1fa8473d13fed8f6bd5493e924c2ed42 Mon Sep 17 00:00:00 2001 From: jajjibhai008 Date: Fri, 15 Sep 2023 19:03:16 +0500 Subject: [PATCH 2/2] feat: show all enterprise budgets regardless of plan and route correctly --- .../EnterpriseApp/data/constants.js | 6 + .../EnterpriseSubsidiesContext/data/hooks.js | 16 +- .../BudgetCard-V2.jsx | 85 ++++++---- .../Budgetcard-V3.jsx | 160 +++++++++--------- .../MultipleBudgetsPicker.jsx | 1 + .../learner-credit-management/data/hooks.js | 2 +- .../data/tests/hooks.test.js | 2 +- .../data/tests/utils.test.js | 6 +- .../learner-credit-management/data/utils.js | 39 +++-- .../tests/BudgetCard.test.jsx | 2 +- 10 files changed, 184 insertions(+), 135 deletions(-) diff --git a/src/components/EnterpriseApp/data/constants.js b/src/components/EnterpriseApp/data/constants.js index 5c881fefab..7bacfbd457 100644 --- a/src/components/EnterpriseApp/data/constants.js +++ b/src/components/EnterpriseApp/data/constants.js @@ -13,3 +13,9 @@ export const ROUTE_NAMES = { subscriptionManagement: 'subscriptions', contentHighlights: 'content-highlights', }; + +export const BUDGET_STATUSES = { + active: 'Active', + expired: 'Expired', + upcoming: 'Upcoming', +}; diff --git a/src/components/EnterpriseSubsidiesContext/data/hooks.js b/src/components/EnterpriseSubsidiesContext/data/hooks.js index 243d6063b8..fc91edb874 100644 --- a/src/components/EnterpriseSubsidiesContext/data/hooks.js +++ b/src/components/EnterpriseSubsidiesContext/data/hooks.js @@ -43,10 +43,10 @@ export const useEnterpriseOffers = ({ enablePortalLearnerCreditManagementScreen, const { isActive } = subsidy; // Always check isActive for enterprise subsidies const isCurrent = isActive; // You can adjust this based on your specific requirements const activeSubsidyData = { - id: subsidy.uuid || subsidy.id, - name: subsidy.title || subsidy.displayName, - start: subsidy.activeDatetime || subsidy.startDatetime, - end: subsidy.expirationDatetime || subsidy.endDatetime, + id: subsidy.uuid, + name: subsidy.title, + start: subsidy.activeDatetime, + end: subsidy.expirationDatetime, isCurrent, source, }; @@ -61,10 +61,10 @@ export const useEnterpriseOffers = ({ enablePortalLearnerCreditManagementScreen, const source = 'ecommerceApi'; const { isCurrent } = subsidy; const activeSubsidyData = { - id: subsidy.uuid || subsidy.id, - name: subsidy.title || subsidy.displayName, - start: subsidy.activeDatetime || subsidy.startDatetime, - end: subsidy.expirationDatetime || subsidy.endDatetime, + id: subsidy.id, + name: subsidy.displayName, + start: subsidy.startDatetime, + end: subsidy.endDatetime, isCurrent, source, }; diff --git a/src/components/learner-credit-management/BudgetCard-V2.jsx b/src/components/learner-credit-management/BudgetCard-V2.jsx index 09a5c8cdf5..11e563e606 100644 --- a/src/components/learner-credit-management/BudgetCard-V2.jsx +++ b/src/components/learner-credit-management/BudgetCard-V2.jsx @@ -1,18 +1,44 @@ +/* eslint-disable */ import React from 'react'; import PropTypes from 'prop-types'; -import dayjs from 'dayjs'; import { Stack, + Col, + Card, + Skeleton, } from '@edx/paragon'; import { useOfferSummary } from './data/hooks'; import SubBudgetCard from './Budgetcard-V3'; +const LoadingCards = () => ( + + + + +
+ +
+
+
+ + + + +
+ +
+
+
+ +
+); const BudgetCard = ({ offer, enterpriseUUID, enterpriseSlug, offerType, + displayName, }) => { const { start, @@ -24,37 +50,35 @@ const BudgetCard = ({ offerSummary, } = useOfferSummary(enterpriseUUID, offer); - const formattedStartDate = dayjs(start).format('MMMM D, YYYY'); - const formattedExpirationDate = dayjs(end).format('MMMM D, YYYY'); - return ( - <> -

Budgets

- {!isLoadingOfferSummary - && offerType === 'ecommerceApi' - ? ( - - ) - : offerSummary?.budgetsSumary.map((budget) => ( - - ))} - +

Budgets

+ {isLoadingOfferSummary ? ( + + ) : offerType === 'ecommerceApi' ? ( + + ) : ( + offerSummary?.budgetsSummary.map((budget) => ( + + )) + )}
); }; @@ -69,6 +93,7 @@ BudgetCard.propTypes = { enterpriseUUID: PropTypes.string.isRequired, enterpriseSlug: PropTypes.string.isRequired, offerType: PropTypes.string.isRequired, + displayName: PropTypes.string, }; export default BudgetCard; diff --git a/src/components/learner-credit-management/Budgetcard-V3.jsx b/src/components/learner-credit-management/Budgetcard-V3.jsx index 97c275b4e7..5f981acc9b 100644 --- a/src/components/learner-credit-management/Budgetcard-V3.jsx +++ b/src/components/learner-credit-management/Budgetcard-V3.jsx @@ -1,99 +1,103 @@ +/* eslint-disable */ import { Link } from 'react-router-dom'; import PropTypes from 'prop-types'; import dayjs from 'dayjs'; import { - Card, - Button, - Stack, - Row, - Col, + Card, + Button, + Stack, + Row, + Col, } from '@edx/paragon'; -import { ROUTE_NAMES } from '../EnterpriseApp/data/constants'; +import { BUDGET_STATUSES, ROUTE_NAMES } from '../EnterpriseApp/data/constants'; +import { getBudgetStatus } from './data/utils'; const SubBudgetCard = ({ - id, - start, - end, - available, - spent, - enterpriseSlug, + id, + start, + end, + available, + spent, + displayName, + enterpriseSlug, }) => { - const formattedStartDate = dayjs(start).format('MMMM D, YYYY'); - const formattedExpirationDate = dayjs(end).format('MMMM D, YYYY'); + const formattedStartDate = dayjs(start).format('MMMM D, YYYY'); + const formattedExpirationDate = dayjs(end).format('MMMM D, YYYY'); + const budgetStatus = getBudgetStatus(start, end); - const renderActions = (id) => ( - - ); - - const renderCardHeader = (budgetType, id) => { - const subtitle = ( -
- - {formattedStartDate} - {formattedExpirationDate} - -
+ const renderActions = (id) => ( + ); - return ( - - {renderActions(id)} - + const renderCardHeader = (budgetType, id) => { + const subtitle = ( +
+ + {formattedStartDate} - {formattedExpirationDate} + +
+ ); + + return ( + + {budgetStatus !== BUDGET_STATUSES.upcoming && renderActions(id)} + )} - /> - ); - }; + /> + ); + }; - const renderCardSection = (available, spent) => ( - - - - Available - {available} - - - Spent - {spent} - - - - ); + const renderCardSection = (available, spent) => ( + + + + Available + {available} + + + Spent + {spent} + + + + ); - return ( - - - - {renderCardHeader('Overview', id)} - {renderCardSection(available, spent)} - - - - ); + return ( + + + + {renderCardHeader(displayName || 'Overview', id)} + {budgetStatus !== BUDGET_STATUSES.upcoming && renderCardSection(available, spent)} + + + + ); }; SubBudgetCard.propTypes = { - enterpriseSlug: PropTypes.string.isRequired, - id: PropTypes.string, - start: PropTypes.string, - end: PropTypes.string, - spent: PropTypes.number, - available: PropTypes.number, - + enterpriseSlug: PropTypes.string.isRequired, + id: PropTypes.string, + start: PropTypes.string, + end: PropTypes.string, + spent: PropTypes.number, + available: PropTypes.number, + displayName: PropTypes.string, }; export default SubBudgetCard; diff --git a/src/components/learner-credit-management/MultipleBudgetsPicker.jsx b/src/components/learner-credit-management/MultipleBudgetsPicker.jsx index 9dc6a227c1..8bedc5dd5e 100644 --- a/src/components/learner-credit-management/MultipleBudgetsPicker.jsx +++ b/src/components/learner-credit-management/MultipleBudgetsPicker.jsx @@ -25,6 +25,7 @@ const MultipleBudgetsPicker = ({ enterpriseSlug={enterpriseSlug} enableLearnerPortal={enableLearnerPortal} offerType={offer.source} + displayName={offer.name} /> ))} diff --git a/src/components/learner-credit-management/data/hooks.js b/src/components/learner-credit-management/data/hooks.js index 5e107e9428..31577f36a7 100644 --- a/src/components/learner-credit-management/data/hooks.js +++ b/src/components/learner-credit-management/data/hooks.js @@ -74,7 +74,7 @@ const applyFiltersToOptions = (filters, options) => { } }; -export const useOfferRedemptions = (enterpriseUUID, offerId= null, budgetId = null) => { +export const useOfferRedemptions = (enterpriseUUID, offerId = null, budgetId = null) => { const shouldTrackFetchEvents = useRef(false); const [isLoading, setIsLoading] = useState(true); const [offerRedemptions, setOfferRedemptions] = useState({ diff --git a/src/components/learner-credit-management/data/tests/hooks.test.js b/src/components/learner-credit-management/data/tests/hooks.test.js index f70f533d4d..5459167674 100644 --- a/src/components/learner-credit-management/data/tests/hooks.test.js +++ b/src/components/learner-credit-management/data/tests/hooks.test.js @@ -73,7 +73,7 @@ describe('useOfferSummary', () => { remainingFunds: 4800, percentUtilized: 0.04, offerId: 1, - budgetsSumary: [], + budgetsSummary: [], offerType: undefined, }; expect(result.current).toEqual({ diff --git a/src/components/learner-credit-management/data/tests/utils.test.js b/src/components/learner-credit-management/data/tests/utils.test.js index b24abe27d4..f39d9f33a9 100644 --- a/src/components/learner-credit-management/data/tests/utils.test.js +++ b/src/components/learner-credit-management/data/tests/utils.test.js @@ -23,7 +23,7 @@ describe('transformOfferSummary', () => { remainingFunds: 0.0, percentUtilized: 1.0, offerType: EXEC_ED_OFFER_TYPE, - budgetsSumary: [], + budgetsSummary: [], offerId: undefined, }); }); @@ -36,7 +36,7 @@ describe('transformOfferSummary', () => { percentOfOfferSpent: null, offerType: 'Site', offerId: '123', - budgetsSumary: [], + budgetsSummary: [], }; expect(transformOfferSummary(offerSummary)).toEqual({ @@ -48,7 +48,7 @@ describe('transformOfferSummary', () => { redeemedFundsExecEd: undefined, redeemedFundsOcm: undefined, offerId: '123', - budgetsSumary: [], + budgetsSummary: [], }); }); }); diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 1c7a52c459..fc193744c2 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -3,6 +3,7 @@ import { LOW_REMAINING_BALANCE_PERCENT_THRESHOLD, NO_BALANCE_REMAINING_DOLLAR_THRESHOLD, } from './constants'; +import { BUDGET_STATUSES } from '../../EnterpriseApp/data/constants'; /** * Transforms offer summary from API for display in the UI, guarding * against bad data (e.g., accounting for refunds). @@ -12,23 +13,22 @@ import { */ export const transformOfferSummary = (offerSummary) => { if (!offerSummary) { return null; } - let budgetsSumary = [] - + const budgetsSummary = []; if (offerSummary?.budgets) { - const budgets = offerSummary?.budgets + const budgets = offerSummary?.budgets; for (let i = 0; i < budgets.length; i++) { - let redeemedFunds = budgets[i].amountOfPolicySpent && parseFloat(budgets[i].amountOfPolicySpent); - let remainingFunds = budgets[i].remainingBalance && parseFloat(budgets[i].remainingBalance); + const redeemedFunds = budgets[i].amountOfPolicySpent && parseFloat(budgets[i].amountOfPolicySpent); + const remainingFunds = budgets[i].remainingBalance && parseFloat(budgets[i].remainingBalance); // Create an object with key-value pairs const budgetEntry = { redeemedFunds, remainingFunds, - ...budgets[i] + ...budgets[i], }; - budgetsSumary.push(budgetEntry); + budgetsSummary.push(budgetEntry); } } - + const totalFunds = offerSummary.maxDiscount && parseFloat(offerSummary.maxDiscount); let redeemedFunds = offerSummary.amountOfOfferSpent && parseFloat(offerSummary.amountOfOfferSpent); let redeemedFundsOcm = offerSummary.amountOfferSpentOcm && parseFloat(offerSummary.amountOfferSpentOcm); @@ -64,7 +64,7 @@ export const transformOfferSummary = (offerSummary) => { percentUtilized, offerType, offerId, - budgetsSumary, + budgetsSummary, }; }; @@ -110,7 +110,20 @@ export const getProgressBarVariant = ({ percentUtilized, remainingFunds }) => { return variant; }; -// Utility function to check if the ID is a UUID -export const isUUID = (id) => { - return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(id); -} +// Utility function to check if the ID is a UUID +export const isUUID = (id) => /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(id); + +// Utility function to check the budget status +export const getBudgetStatus = (startDateStr, endDateStr) => { + const currentDate = new Date(); + const startDate = new Date(startDateStr); + const endDate = new Date(endDateStr); + + if (currentDate < startDate) { + return BUDGET_STATUSES.upcoming; + } + if (currentDate >= startDate && currentDate <= endDate) { + return BUDGET_STATUSES.active; + } + return BUDGET_STATUSES.expired; +}; diff --git a/src/components/learner-credit-management/tests/BudgetCard.test.jsx b/src/components/learner-credit-management/tests/BudgetCard.test.jsx index dcff64ce3e..bd445a8676 100644 --- a/src/components/learner-credit-management/tests/BudgetCard.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetCard.test.jsx @@ -81,7 +81,7 @@ describe('', () => { remainingFunds: 4800, percentUtilized: 0.04, offerType: 'Site', - budgetsSumary: [ + budgetsSummary: [ { id: 123, start: '2022-01-01',