From d67bee6ef759e5331b779a153a9f53aec407ca8a Mon Sep 17 00:00:00 2001 From: Cyril Nxumalo Date: Tue, 21 May 2024 17:08:01 +0200 Subject: [PATCH] feat: Retired workflow on the list policy screens --- .../learner-credit-management/BudgetCard.jsx | 3 + .../SubBudgetCard.jsx | 151 +++++++++++------- .../learner-credit-management/data/utils.js | 14 +- .../tests/BudgetCard.test.jsx | 145 ++++++++++++++++- src/index.scss | 4 + 5 files changed, 250 insertions(+), 67 deletions(-) diff --git a/src/components/learner-credit-management/BudgetCard.jsx b/src/components/learner-credit-management/BudgetCard.jsx index 5eb4f2d678..fb9807701c 100644 --- a/src/components/learner-credit-management/BudgetCard.jsx +++ b/src/components/learner-credit-management/BudgetCard.jsx @@ -26,6 +26,7 @@ const BudgetCard = ({ original }) => { id, isAssignable, isRetired, + retiredAt, name, source, start, @@ -51,6 +52,7 @@ const BudgetCard = ({ original }) => { enterpriseSlug={enterpriseSlug} isAssignable={isAssignable} isRetired={isRetired} + retiredAt={retiredAt} /> ); } @@ -107,6 +109,7 @@ BudgetCard.propTypes = { }), isAssignable: PropTypes.bool, isRetired: PropTypes.bool, + retiredAt: PropTypes.string, enterpriseUUID: PropTypes.string.isRequired, enterpriseSlug: PropTypes.string.isRequired, status: PropTypes.string, diff --git a/src/components/learner-credit-management/SubBudgetCard.jsx b/src/components/learner-credit-management/SubBudgetCard.jsx index d481ed48c7..804e87081c 100644 --- a/src/components/learner-credit-management/SubBudgetCard.jsx +++ b/src/components/learner-credit-management/SubBudgetCard.jsx @@ -68,6 +68,7 @@ const BaseSubBudgetCard = ({ isLoading, isAssignable, isRetired, + retiredAt, }) => { const { isFetching: isFetchingBudgets } = useEnterpriseBudgets({ enablePortalLearnerCreditManagementScreen, @@ -79,6 +80,7 @@ const BaseSubBudgetCard = ({ startDateStr: start, endDateStr: end, isBudgetRetired: isRetired, + retiredDateStr: retiredAt, }); const formattedDate = budgetLabel?.date ? intl.formatDate( dayjs(budgetLabel?.date).toDate(), @@ -89,6 +91,16 @@ const BaseSubBudgetCard = ({ }, ) : undefined; + const hasBudgetAggregatesSection = () => { + const { status } = budgetLabel; + + return ( + status === BUDGET_STATUSES.active + || status === BUDGET_STATUSES.expiring + || status === BUDGET_STATUSES.retired + ); + }; + const renderActions = (budgetId) => ( ); @@ -116,88 +136,94 @@ const BaseSubBudgetCard = ({ ); + const showActions = budgetLabel.status !== BUDGET_STATUSES.scheduled; + return ( {budgetType}} subtitle={{subtitle}} - actions={ - budgetLabel.status !== BUDGET_STATUSES.scheduled - ? renderActions(budgetId) - : undefined - } - className={classNames('align-items-center', { - 'mb-4.5': budgetLabel.status !== BUDGET_STATUSES.active && budgetLabel.status !== BUDGET_STATUSES.expiring, - })} + actions={showActions ? renderActions(budgetId) : undefined} + className={classNames('align-items-center', { 'mb-4.5': !hasBudgetAggregatesSection })} /> ); }; - const renderCardSection = () => ( - - - -)} - muted - > - - -
+ const renderCardSection = () => { + if (!hasBudgetAggregatesSection()) { + return null; + } + + return ( + -
- - {isFetchingBudgets ? : formatPrice(available)} - - - {isAssignable && ( - -
+ + )} + muted + > + + {!isRetired && ( + <> + +
+ +
+ + {isFetchingBudgets ? : formatPrice(available)} + + + {isAssignable && ( + +
+ +
+ + {isFetchingBudgets ? : formatPrice(pending)} + + + )} + + )} + +
- - {isFetchingBudgets ? : formatPrice(pending)} + + {isFetchingBudgets ? : formatPrice(spent)} - )} - -
- -
- - {isFetchingBudgets ? : formatPrice(spent)} - - - - ); + + ); + }; return ( {renderCardHeader(displayName || 'Overview', id)} - {(budgetLabel.status === BUDGET_STATUSES.active || budgetLabel.status === BUDGET_STATUSES.expiring) - && renderCardSection()} + {renderCardSection()} @@ -219,6 +245,7 @@ BaseSubBudgetCard.propTypes = { displayName: PropTypes.string, isAssignable: PropTypes.bool, isRetired: PropTypes.bool, + retiredAt: PropTypes.string, }; BaseSubBudgetCard.defaultProps = { diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 2fc358df5a..65e86ec30e 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -163,6 +163,7 @@ export const getBudgetStatus = ({ endDateStr, isBudgetRetired, currentDate = new Date(), + retiredDateStr = null, }) => { const startDate = new Date(startDateStr); const endDate = new Date(endDateStr); @@ -171,10 +172,9 @@ export const getBudgetStatus = ({ if (isBudgetRetired) { return { status: BUDGET_STATUSES.retired, - badgeVariant: 'info', - // no term or date for retired budgets - term: null, - date: null, + badgeVariant: 'light', + term: 'Retired', + date: retiredDateStr, }; } @@ -532,6 +532,12 @@ export const getTranslatedBudgetTerm = (intl, term) => { defaultMessage: 'Expired', description: 'Term for when a budget has expired', }); + case 'Retired': + return intl.formatMessage({ + id: 'lcm.budgets.budget.card.term.retired', + defaultMessage: 'Retired', + description: 'Term for when a budget has retired', + }); default: return ''; } diff --git a/src/components/learner-credit-management/tests/BudgetCard.test.jsx b/src/components/learner-credit-management/tests/BudgetCard.test.jsx index 43687867db..7b24d2c4bf 100644 --- a/src/components/learner-credit-management/tests/BudgetCard.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetCard.test.jsx @@ -7,6 +7,7 @@ import dayjs from 'dayjs'; import { screen, render, + within, } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; @@ -14,7 +15,7 @@ import { IntlProvider } from '@edx/frontend-platform/i18n'; import { QueryClientProvider } from '@tanstack/react-query'; import BudgetCard from '../BudgetCard'; import { formatPrice, useSubsidySummaryAnalyticsApi, useBudgetRedemptions } from '../data'; -import { BUDGET_TYPES } from '../../EnterpriseApp/data/constants'; +import { BUDGET_STATUSES, BUDGET_TYPES } from '../../EnterpriseApp/data/constants'; import { EnterpriseSubsidiesContext } from '../../EnterpriseSubsidiesContext'; import { queryClient } from '../../test/testUtils'; @@ -547,4 +548,146 @@ describe('', () => { expect(screen.getByText('Spent')).toBeInTheDocument(); expect(screen.getByText(formatPrice(mockBudgetAggregates.spent))).toBeInTheDocument(); }); + + it('displays correctly for a retired Policy (enterprise-access) (%s)', () => { + const mockBudgetAggregates = { + total: 5000, + spent: 200, + pending: undefined, + available: 4800, + }; + const mockBudget = { + id: mockBudgetUuid, + name: mockBudgetDisplayName, + start: '2022-01-01', + end: '3023-01-01', + source: BUDGET_TYPES.policy, + aggregates: { + available: mockBudgetAggregates.available, + pending: mockBudgetAggregates.pending, + spent: mockBudgetAggregates.spent, + }, + isAssignable: false, + enterpriseSlug, + enterpriseUUID, + isRetired: true, + retiredAt: '2022-01-01', + }; + + useSubsidySummaryAnalyticsApi.mockReturnValue({ + isLoading: false, + subsidySummary: undefined, + }); + + render(); + + // Assertions for budget card display + expect(screen.getByText(mockBudgetDisplayName)).toBeInTheDocument(); + expect(screen.queryByText('Executive Education')).not.toBeInTheDocument(); + + const formattedString = `Retired ${dayjs(mockBudget.retiredAt).format('MMMM D, YYYY')}`; + const elementsWithTestId = screen.getAllByTestId('budget-date'); + const firstElementWithTestId = elementsWithTestId[0]; + expect(firstElementWithTestId).toHaveTextContent(formattedString); + + // Verify 'View budget' CTA + const viewBudgetCTA = screen.getByText('View budget history', { selector: 'a' }); + expect(viewBudgetCTA).toBeInTheDocument(); // Ensure 'View budget' CTA is present + expect(viewBudgetCTA).toHaveAttribute( + 'href', + `/${enterpriseSlug}/admin/learner-credit/${mockBudgetUuid}`, + ); + + const balanceDetailSection = screen.getByTestId('balance-detail-section'); + + // Assertions for aggregates display + expect(within(balanceDetailSection).queryByText('Balance')).not.toBeInTheDocument(); + expect(within(balanceDetailSection).queryByText('Available')).not.toBeInTheDocument(); + expect(within(balanceDetailSection).getByText('Spent')).toBeInTheDocument(); + expect(within(balanceDetailSection).getByText(formatPrice(mockBudgetAggregates.spent))).toBeInTheDocument(); + }); + + it.each([ + { + status: BUDGET_STATUSES.active, + shouldShow: true, + start: '2022-01-01', + end: '3023-01-01', + retiredAt: null, + }, + { + status: BUDGET_STATUSES.retired, + shouldShow: true, + start: '2022-01-01', + end: '3023-01-01', + retiredAt: '2022-05-01', + }, + { + status: BUDGET_STATUSES.expiring, + shouldShow: true, + start: '2022-01-01', + end: dayjs().add(30, 'day').toString(), + retiredAt: null, + }, + { + status: BUDGET_STATUSES.scheduled, + shouldShow: false, + start: '3022-01-01', + end: '3023-01-01', + retiredAt: null, + }, + { + status: BUDGET_STATUSES.expired, + shouldShow: false, + start: '2022-01-01', + end: '2023-01-01', + retiredAt: null, + }, + ])('only render aggregates section for appropriate statuses (%s)', ( + { + shouldShow, + start, + end, + retiredAt, + }, + ) => { + const mockBudgetAggregates = { + total: 5000, + spent: 200, + pending: undefined, + available: 4800, + }; + const mockBudget = { + id: mockBudgetUuid, + name: mockBudgetDisplayName, + start, + end, + source: BUDGET_TYPES.policy, + aggregates: { + available: mockBudgetAggregates.available, + pending: mockBudgetAggregates.pending, + spent: mockBudgetAggregates.spent, + }, + isAssignable: false, + enterpriseSlug, + enterpriseUUID, + isRetired: !!retiredAt, + retiredAt, + }; + + useSubsidySummaryAnalyticsApi.mockReturnValue({ + isLoading: false, + subsidySummary: undefined, + }); + + render(); + + const aggregatesSection = screen.queryByTestId('aggregates-section'); + + if (shouldShow) { + expect(aggregatesSection).toBeInTheDocument(); + } else { + expect(aggregatesSection).not.toBeInTheDocument(); + } + }); }); diff --git a/src/index.scss b/src/index.scss index c60d3ff013..f38108a463 100644 --- a/src/index.scss +++ b/src/index.scss @@ -79,3 +79,7 @@ form { .stepper-modal .pgn__modal-header { border-bottom: solid 7px; } + +.font-size-base { + font-size: $font-size-base !important; +}