diff --git a/src/components/EnterpriseApp/data/constants.js b/src/components/EnterpriseApp/data/constants.js index 6feaac51f7..1773fd73e2 100644 --- a/src/components/EnterpriseApp/data/constants.js +++ b/src/components/EnterpriseApp/data/constants.js @@ -17,7 +17,7 @@ export const ROUTE_NAMES = { export const BUDGET_STATUSES = { active: 'Active', expired: 'Expired', - upcoming: 'Upcoming', + scheduled: 'Scheduled', }; export const BUDGET_TYPES = { diff --git a/src/components/learner-credit-management/MultipleBudgetsPicker.jsx b/src/components/learner-credit-management/MultipleBudgetsPicker.jsx index db6f178f2a..44407531c0 100644 --- a/src/components/learner-credit-management/MultipleBudgetsPicker.jsx +++ b/src/components/learner-credit-management/MultipleBudgetsPicker.jsx @@ -7,36 +7,40 @@ import { } from '@edx/paragon'; import BudgetCard from './BudgetCard-V2'; +import { orderOffers } from './data/utils'; const MultipleBudgetsPicker = ({ offers, enterpriseUUID, enterpriseSlug, enableLearnerPortal, -}) => ( - - - Budgets - - - - - {offers.map(offer => ( - - ))} - - - - -); +}) => { + const orderedOffers = orderOffers(offers); + return ( + + + Budgets + + + + + {orderedOffers?.map(offer => ( + + ))} + + + + + ); +}; MultipleBudgetsPicker.propTypes = { offers: PropTypes.arrayOf(PropTypes.shape()).isRequired, diff --git a/src/components/learner-credit-management/SubBudgetCard.jsx b/src/components/learner-credit-management/SubBudgetCard.jsx index d3360cea43..3841aebea3 100644 --- a/src/components/learner-credit-management/SubBudgetCard.jsx +++ b/src/components/learner-credit-management/SubBudgetCard.jsx @@ -6,6 +6,8 @@ import { Button, Row, Col, + Badge, + Stack, } from '@edx/paragon'; import { BUDGET_STATUSES, ROUTE_NAMES } from '../EnterpriseApp/data/constants'; @@ -21,9 +23,8 @@ const SubBudgetCard = ({ enterpriseSlug, isLoading, }) => { - const formattedStartDate = dayjs(start).format('MMMM D, YYYY'); - const formattedExpirationDate = dayjs(end).format('MMMM D, YYYY'); - const budgetStatus = getBudgetStatus(start, end); + const budgetLabel = getBudgetStatus(start, end); + const formattedDate = dayjs(budgetLabel?.date).format('MMMM D, YYYY'); const renderActions = (budgetId) => ( { const subtitle = ( - + + {budgetLabel.status} - {formattedStartDate} - {formattedExpirationDate} + {budgetLabel.term} {formattedDate} - + ); return ( @@ -50,10 +52,10 @@ const SubBudgetCard = ({ subtitle={subtitle} className="mb-3" actions={ - budgetStatus !== BUDGET_STATUSES.upcoming - ? renderActions(budgetId) - : undefined - } + budgetLabel.status !== BUDGET_STATUSES.scheduled + ? renderActions(budgetId) + : undefined + } /> ); }; @@ -83,7 +85,7 @@ const SubBudgetCard = ({ > {renderCardHeader(displayName || 'Overview', id)} - {budgetStatus !== BUDGET_STATUSES.upcoming && renderCardSection(available, spent)} + {budgetLabel.status !== BUDGET_STATUSES.scheduled && renderCardSection(available, spent)} ); 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 ab19a94d27..fa4ec2bb23 100644 --- a/src/components/learner-credit-management/data/tests/utils.test.js +++ b/src/components/learner-credit-management/data/tests/utils.test.js @@ -1,4 +1,4 @@ -import { transformOfferSummary, getBudgetStatus } from '../utils'; +import { transformOfferSummary, getBudgetStatus, orderOffers } from '../utils'; import { EXEC_ED_OFFER_TYPE } from '../constants'; describe('transformOfferSummary', () => { @@ -93,26 +93,77 @@ describe('transformOfferSummary', () => { }); describe('getBudgetStatus', () => { - it('should return "upcoming" when the current date is before the start date', () => { - const startDateStr = '2023-09-30'; - const endDateStr = '2023-10-30'; + it('should return "Scheduled" when the current date is before the start date', () => { + const startDateStr = '2024-09-30'; + const endDateStr = '2027-10-30'; const currentDateStr = '2023-09-28'; const result = getBudgetStatus(startDateStr, endDateStr, new Date(currentDateStr)); - expect(result).toEqual('Upcoming'); + expect(result.status).toEqual('Scheduled'); }); - it('should return "active" when the current date is between the start and end dates', () => { - const startDateStr = '2023-09-01'; - const endDateStr = '2023-09-30'; - const currentDateStr = '2023-09-05'; + it('should return "Active" when the current date is between the start and end dates', () => { + const startDateStr = '2023-08-01'; + const endDateStr = '2027-10-30'; + const currentDateStr = '2023-09-28'; const result = getBudgetStatus(startDateStr, endDateStr, new Date(currentDateStr)); - expect(result).toEqual('Active'); + expect(result.status).toEqual('Active'); }); - it('should return "expired" when the current date is after the end date', () => { + it('should return "Expired" when the current date is after the end date', () => { const startDateStr = '2023-08-01'; const endDateStr = '2023-08-31'; - const result = getBudgetStatus(startDateStr, endDateStr); - expect(result).toEqual('Expired'); + const currentDateStr = '2023-09-28'; + const result = getBudgetStatus(startDateStr, endDateStr, new Date(currentDateStr)); + expect(result.status).toEqual('Expired'); + }); +}); + +// Example offer objects for testing +const offers = [ + { + name: 'Offer 1', + start: '2023-01-01T00:00:00Z', + end: '2023-01-10T00:00:00Z', + }, + { + name: 'Offer 2', + start: '2022-12-01T00:00:00Z', + end: '2022-12-20T00:00:00Z', + }, + { + name: 'Offer 3', + start: '2023-02-01T00:00:00Z', + end: '2023-02-15T00:00:00Z', + }, + { + name: 'Offer 4', + start: '2023-01-15T00:00:00Z', + end: '2023-01-25T00:00:00Z', + }, +]; + +describe('orderOffers', () => { + it('should sort offers correctly', () => { + const sortedOffers = orderOffers(offers); + + // Expected order: Active offers (Offer 2), Upcoming offers (Offer 1, Offer 4), Expired offers (Offer 3) + expect(sortedOffers.map((offer) => offer.name)).toEqual(['Offer 2', 'Offer 1', 'Offer 4', 'Offer 3']); + }); + + it('should handle empty input', () => { + const sortedOffers = orderOffers([]); + expect(sortedOffers).toEqual([]); + }); + + it('should handle offers with the same status and end date', () => { + const duplicateOffers = [ + { name: 'Offer A', start: '2023-01-01T00:00:00Z', end: '2023-01-15T00:00:00Z' }, + { name: 'Offer B', start: '2023-01-01T00:00:00Z', end: '2023-01-15T00:00:00Z' }, + ]; + + const sortedOffers = orderOffers(duplicateOffers); + + // Since both offers have the same status ("active") and end date, they should be sorted alphabetically by name. + expect(sortedOffers.map((offer) => offer.name)).toEqual(['Offer A', 'Offer B']); }); }); diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index c42cb4039c..376479f6a5 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -118,12 +118,27 @@ export const getBudgetStatus = (startDateStr, endDateStr, currentDate = new Date const endDate = new Date(endDateStr); if (currentDate < startDate) { - return BUDGET_STATUSES.upcoming; + return { + status: BUDGET_STATUSES.scheduled, + badgeVariant: 'secondary', + term: 'Starts', + date: startDateStr, + }; } if (currentDate >= startDate && currentDate <= endDate) { - return BUDGET_STATUSES.active; + return { + status: BUDGET_STATUSES.active, + badgeVariant: 'primary', + term: 'Expires', + date: endDateStr, + }; } - return BUDGET_STATUSES.expired; + return { + status: BUDGET_STATUSES.expired, + badgeVariant: 'light', + term: 'Expired', + date: endDateStr, + }; }; export const formatPrice = (price) => { @@ -133,3 +148,36 @@ export const formatPrice = (price) => { }); return USDollar.format(Math.abs(price)); }; + +/** + * Orders a list of offers based on their status, end date, and name. + * Active offers come first, followed by scheduled offers, and then expired offers. + * Within each status, offers are sorted by their end date and name. + * + * @param {Array} offers - An array of offer objects. + * @returns {Array} - The sorted array of offer objects. + */ +export const orderOffers = (offers) => { + const statusOrder = { + Active: 0, + Scheduled: 1, + Expired: 2, + }; + + offers?.sort((offerA, offerB) => { + const statusA = getBudgetStatus(offerA.start, offerA.end).status; + const statusB = getBudgetStatus(offerB.start, offerB.end).status; + + if (statusOrder[statusA] !== statusOrder[statusB]) { + return statusOrder[statusA] - statusOrder[statusB]; + } + + if (offerA.end !== offerB.end) { + return offerA.end.localeCompare(offerB.end); + } + + return offerA.name.localeCompare(offerB.name); + }); + + return offers; +}; diff --git a/src/components/learner-credit-management/tests/BudgetCard.test.jsx b/src/components/learner-credit-management/tests/BudgetCard.test.jsx index d8aa511a4a..c1b20b9958 100644 --- a/src/components/learner-credit-management/tests/BudgetCard.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetCard.test.jsx @@ -110,7 +110,7 @@ describe('', () => { />); expect(screen.getByText('Overview')); expect(screen.queryByText('Executive Education')).not.toBeInTheDocument(); - const formattedString = `${dayjs(mockOffer.start).format('MMMM D, YYYY')} - ${dayjs(mockOffer.end).format('MMMM D, YYYY')}`; + const formattedString = `Expired ${dayjs(mockOffer.end).format('MMMM D, YYYY')}`; const elementsWithTestId = screen.getAllByTestId('offer-date'); const firstElementWithTestId = elementsWithTestId[0]; expect(firstElementWithTestId).toHaveTextContent(formattedString); diff --git a/src/components/learner-credit-management/tests/MultipleBudgetsPage.test.jsx b/src/components/learner-credit-management/tests/MultipleBudgetsPage.test.jsx index 8b22a9d4ec..785da8899d 100644 --- a/src/components/learner-credit-management/tests/MultipleBudgetsPage.test.jsx +++ b/src/components/learner-credit-management/tests/MultipleBudgetsPage.test.jsx @@ -23,10 +23,21 @@ const initialStore = { const store = getMockStore({ ...initialStore }); const enterpriseUUID = '1234'; -const defaultEnterpriseSubsidiesContextValue = { - offers: [], +const emptyOffersContextValue = { + offers: [], // Empty offers array }; +const defaultEnterpriseSubsidiesContextValue = { + offers: [{ + source: 'subsidy', + id: '392f1fe1-ee91-4f44-b174-13ecf59866eb', + name: 'Subsidy 2 for Executive Education (2U) Integration QA', + start: '2023-06-07T15:38:29Z', + end: '2024-06-07T15:38:30Z', + isCurrent: true, + }, + ], +}; const MultipleBudgetsPageWrapper = ({ enterpriseSubsidiesContextValue = defaultEnterpriseSubsidiesContextValue, ...rest @@ -40,8 +51,16 @@ const MultipleBudgetsPageWrapper = ({ describe('', () => { it('No budgets for your organization', () => { - render(); + render(); expect(screen.getByText('No budgets for your organization')); expect(screen.getByText('Contact support')); }); + it('budgets for your organization', () => { + render(); + expect(screen.getByText('Budgets')); + }); });