Skip to content

Commit

Permalink
feat: label and order each budget card on lCM page
Browse files Browse the repository at this point in the history
  • Loading branch information
jajjibhai008 committed Oct 3, 2023
1 parent 47938ad commit 4d7cb88
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 56 deletions.
2 changes: 1 addition & 1 deletion src/components/EnterpriseApp/data/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const ROUTE_NAMES = {
export const BUDGET_STATUSES = {
active: 'Active',
expired: 'Expired',
upcoming: 'Upcoming',
scheduled: 'Scheduled',
};

export const BUDGET_TYPES = {
Expand Down
52 changes: 28 additions & 24 deletions src/components/learner-credit-management/MultipleBudgetsPicker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,40 @@ import {
} from '@edx/paragon';

import BudgetCard from './BudgetCard-V2';
import { orderOffers } from './data/utils';

const MultipleBudgetsPicker = ({
offers,
enterpriseUUID,
enterpriseSlug,
enableLearnerPortal,
}) => (
<Stack gap={4}>
<Row>
<Col lg="12"><h2>Budgets</h2></Col>
</Row>
<Row>
<Col lg="12">
<Stack gap={4}>
{offers.map(offer => (
<BudgetCard
key={offer.id}
offer={offer}
enterpriseUUID={enterpriseUUID}
enterpriseSlug={enterpriseSlug}
enableLearnerPortal={enableLearnerPortal}
offerType={offer.source}
displayName={offer.name}
/>
))}
</Stack>
</Col>
</Row>
</Stack>
);
}) => {
const orderedOffers = orderOffers(offers);
return (
<Stack gap={4}>
<Row>
<Col lg="12"><h2>Budgets</h2></Col>
</Row>
<Row>
<Col lg="12">
<Stack gap={4}>
{orderedOffers?.map(offer => (
<BudgetCard
key={offer.id}
offer={offer}
enterpriseUUID={enterpriseUUID}
enterpriseSlug={enterpriseSlug}
enableLearnerPortal={enableLearnerPortal}
offerType={offer.source}
displayName={offer.name}
/>
))}
</Stack>
</Col>
</Row>
</Stack>
);
};

MultipleBudgetsPicker.propTypes = {
offers: PropTypes.arrayOf(PropTypes.shape()).isRequired,
Expand Down
24 changes: 13 additions & 11 deletions src/components/learner-credit-management/SubBudgetCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
Button,
Row,
Col,
Badge,
Stack,
} from '@edx/paragon';

import { BUDGET_STATUSES, ROUTE_NAMES } from '../EnterpriseApp/data/constants';
Expand All @@ -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) => (
<Button
Expand All @@ -37,11 +38,12 @@ const SubBudgetCard = ({

const renderCardHeader = (budgetType, budgetId) => {
const subtitle = (
<div className="d-flex flex-wrap align-items-center">
<Stack direction="horizontal" gap={2.5}>
<Badge variant={budgetLabel.badgeVariant}>{budgetLabel.status}</Badge>
<span data-testid="offer-date">
{formattedStartDate} - {formattedExpirationDate}
{budgetLabel.term} {formattedDate}
</span>
</div>
</Stack>
);

return (
Expand All @@ -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
}
/>
);
};
Expand Down Expand Up @@ -83,7 +85,7 @@ const SubBudgetCard = ({
>
<Card.Body>
{renderCardHeader(displayName || 'Overview', id)}
{budgetStatus !== BUDGET_STATUSES.upcoming && renderCardSection(available, spent)}
{budgetLabel.status !== BUDGET_STATUSES.scheduled && renderCardSection(available, spent)}
</Card.Body>
</Card>
);
Expand Down
77 changes: 64 additions & 13 deletions src/components/learner-credit-management/data/tests/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { transformOfferSummary, getBudgetStatus } from '../utils';
import { transformOfferSummary, getBudgetStatus, orderOffers } from '../utils';
import { EXEC_ED_OFFER_TYPE } from '../constants';

describe('transformOfferSummary', () => {
Expand Down Expand Up @@ -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']);
});
});
54 changes: 51 additions & 3 deletions src/components/learner-credit-management/data/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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;
};
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ describe('<BudgetCard />', () => {
/>);
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -40,8 +51,16 @@ const MultipleBudgetsPageWrapper = ({

describe('<MultipleBudgetsPage />', () => {
it('No budgets for your organization', () => {
render(<MultipleBudgetsPageWrapper enterpriseUUID={enterpriseUUID} enterpriseSlug={enterpriseId} />);
render(<MultipleBudgetsPageWrapper
enterpriseUUID={enterpriseUUID}
enterpriseSlug={enterpriseId}
enterpriseSubsidiesContextValue={emptyOffersContextValue}
/>);
expect(screen.getByText('No budgets for your organization'));
expect(screen.getByText('Contact support'));
});
it('budgets for your organization', () => {
render(<MultipleBudgetsPageWrapper enterpriseUUID={enterpriseUUID} enterpriseSlug={enterpriseId} />);
expect(screen.getByText('Budgets'));
});
});

0 comments on commit 4d7cb88

Please sign in to comment.