Skip to content

Commit

Permalink
feat: adding new budget headers
Browse files Browse the repository at this point in the history
  • Loading branch information
kiram15 committed Apr 5, 2024
1 parent f85826b commit dbe78ba
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 55 deletions.
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import React from 'react';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Stack, Skeleton, useToggle } from '@edx/paragon';
import { Stack, Skeleton } from '@edx/paragon';

import BudgetDetailRedemptions from './BudgetDetailRedemptions';
import BudgetDetailAssignments from './BudgetDetailAssignments';
import BudgetDetailRedemptions from './BudgetDetailRedemptions';
import { BudgetDetailPageContext } from './BudgetDetailPageWrapper';
import { useBudgetDetailActivityOverview, useBudgetId, useSubsidyAccessPolicy } from './data';
import NoAssignableBudgetActivity from './empty-state/NoAssignableBudgetActivity';
import NoBnEBudgetActivity from './empty-state/NoBnEBudgetActivity';
import InviteMembersModalWrapper from './invite-modal/InviteMembersModalWrapper';

const BudgetDetailActivityTabContents = ({ enterpriseUUID, enterpriseFeatures }) => {
const [inviteModalIsOpen, openInviteModal, closeInviteModal] = useToggle(false);
const isTopDownAssignmentEnabled = enterpriseFeatures.topDownAssignmentRealTimeLcm;
const isEnterpriseGroupsEnabled = enterpriseFeatures.enterpriseGroupsV1;

const {
inviteModalIsOpen, openInviteModal, closeInviteModal,
} = useContext(BudgetDetailPageContext);
const { enterpriseOfferId, subsidyAccessPolicyId } = useBudgetId();
const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId);
const {
Expand Down
36 changes: 11 additions & 25 deletions src/components/learner-credit-management/BudgetDetailPageHeader.jsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,20 @@
import React from 'react';
import {
Stack, Card, Badge, Skeleton,
} from '@edx/paragon';

import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Card, Skeleton, Stack } from '@edx/paragon';

import {
useBudgetId,
useSubsidyAccessPolicy,
useBudgetDetailHeaderData,
useEnterpriseOffer,
formatDate,
useSubsidySummaryAnalyticsApi,
} from './data';

import BudgetDetailPageBreadcrumbs from './BudgetDetailPageBreadcrumbs';
import BudgetDetailPageOverviewAvailability from './BudgetDetailPageOverviewAvailability';
import BudgetDetailPageOverviewUtilization from './BudgetDetailPageOverviewUtilization';
import { BUDGET_TYPES } from '../EnterpriseApp/data/constants';

const BudgetStatusBadge = ({
badgeVariant, status, term, date,
}) => (
<Stack direction="horizontal" gap={2}>
<Badge variant={badgeVariant}>{status}</Badge>
{(term && date) && (
<span className="small">{term} {formatDate(date)}</span>
)}
</Stack>
);
import BudgetStatusSubtitle from './BudgetStatusSubtitle';

const BudgetDetailPageHeader = ({ enterpriseUUID, enterpriseFeatures }) => {
const { subsidyAccessPolicyId, enterpriseOfferId } = useBudgetId();
Expand Down Expand Up @@ -77,7 +63,14 @@ const BudgetDetailPageHeader = ({ enterpriseUUID, enterpriseFeatures }) => {
<Card className="budget-overview-card">
<Card.Section>
<h2>{budgetDisplayName}</h2>
<BudgetStatusBadge badgeVariant={badgeVariant} status={status} term={term} date={date} />
<BudgetStatusSubtitle
badgeVariant={badgeVariant}
status={status}
isAssignable={isAssignable}
term={term}
date={date}
policy={subsidyAccessPolicy}
/>
<BudgetDetailPageOverviewAvailability
budgetId={budgetId}
budgetTotalSummary={budgetTotalSummary}
Expand Down Expand Up @@ -107,11 +100,4 @@ BudgetDetailPageHeader.propTypes = {
}).isRequired,
};

BudgetStatusBadge.propTypes = {
badgeVariant: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
term: PropTypes.string.isRequired,
date: PropTypes.string.isRequired,
};

export default connect(mapStateToProps)(BudgetDetailPageHeader);
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import React from 'react';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { connect } from 'react-redux';
import { generatePath, useParams, Link } from 'react-router-dom';
import {
Button, Col, Hyperlink, ProgressBar, Row, Stack, useMediaQuery, breakpoints,
Button, Col, ProgressBar, Row, Stack,
} from '@edx/paragon';
import { Add } from '@edx/paragon/icons';
import { generatePath, useParams, Link } from 'react-router-dom';
import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils';

import { BudgetDetailPageContext } from './BudgetDetailPageWrapper';
import { formatPrice, useBudgetId, useSubsidyAccessPolicy } from './data';
import { configuration } from '../../config';
import useEnterpriseGroup from './data/hooks/useEnterpriseGroup';
import EVENT_NAMES from '../../eventTracking';
import { LEARNER_CREDIT_ROUTE } from './constants';

Expand Down Expand Up @@ -43,9 +44,10 @@ BudgetDetail.propTypes = {

const BudgetActions = ({ budgetId, isAssignable, enterpriseId }) => {
const { enterpriseSlug, enterpriseAppPage } = useParams();
const supportUrl = configuration.ENTERPRISE_SUPPORT_URL;
const { subsidyAccessPolicyId } = useBudgetId();
const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId);
const { data: appliesToAllContexts } = useEnterpriseGroup(subsidyAccessPolicy);
const { openInviteModal } = useContext(BudgetDetailPageContext);

const trackEventMetadata = {};
if (subsidyAccessPolicy) {
Expand All @@ -65,29 +67,38 @@ const BudgetActions = ({ budgetId, isAssignable, enterpriseId }) => {
);
}

const isLargeScreenOrGreater = useMediaQuery({ query: `(min-width: ${breakpoints.small.minWidth}px)` });

if (!isAssignable) {
if (appliesToAllContexts === true) {
return (
<div className="h-100 d-flex align-items-center pt-4 pt-lg-0">
<div>
<h3>Manage edX for your organization</h3>
<p>
All people in your organization can choose what to learn
from the catalog and spend from the available balance to enroll.
</p>
<Link to={`/${enterpriseSlug}/admin/settings/access`}>
<Button variant="outline-primary">Configure access</Button>
</Link>,
</div>
</div>
);
}
return (
<div className="h-100 d-flex align-items-center pt-4 pt-lg-0">
<div>
<h4>Get people learning using this budget</h4>
<h3>Drive learner-led enrollments by inviting members</h3>
<p>
Funds from this budget are set to auto-allocate to registered learners based on
settings configured with your support team.
Members of this budget can choose what to learn from the catalog
and spend from the available balance to enroll.
</p>
<Button
variant="outline-primary"
as={Hyperlink}
destination={supportUrl}
onClick={() => sendEnterpriseTrackEvent(
enterpriseId,
EVENT_NAMES.LEARNER_CREDIT_MANAGEMENT.BUDGET_OVERVIEW_CONTACT_US,
trackEventMetadata,
)}
variant="brand"
onClick={openInviteModal}
target="_blank"
iconBefore={Add}
>
Contact support
New members
</Button>
</div>
</div>
Expand All @@ -96,9 +107,11 @@ const BudgetActions = ({ budgetId, isAssignable, enterpriseId }) => {

return (
<div className="h-100 d-flex align-items-center justify-content-center pt-4 pt-lg-0">
<div className={classNames({ 'text-center': isLargeScreenOrGreater })}>
<h4>Get people learning using this budget</h4>
<div>
<h3>Lead the way to learning that matters</h3>
<p>Assign content to people using the available budget to cover the cost of enrollment.</p>
<Button
variant="brand"
className="mt-3"
iconBefore={Add}
as={Link}
Expand All @@ -112,7 +125,7 @@ const BudgetActions = ({ budgetId, isAssignable, enterpriseId }) => {
trackEventMetadata,
)}
>
New course assignment
New assignment
</Button>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import { Container, Toast } from '@edx/paragon';
import { Container, Toast, useToggle } from '@edx/paragon';

import Hero from '../Hero';
import {
Expand Down Expand Up @@ -68,12 +68,20 @@ const BudgetDetailPageWrapper = ({
closeToastForInvitation,
} = successfulInvitationToast;

const [inviteModalIsOpen, openInviteModal, closeInviteModal] = useToggle(false);

const values = useMemo(() => ({
successfulAssignmentToast,
successfulCancellationToast,
successfulReminderToast,
successfulInvitationToast,
}), [successfulAssignmentToast, successfulCancellationToast, successfulReminderToast, successfulInvitationToast]);
inviteModalIsOpen,
openInviteModal,
closeInviteModal,
}), [
successfulAssignmentToast, successfulCancellationToast,
successfulReminderToast, successfulInvitationToast,
inviteModalIsOpen, openInviteModal, closeInviteModal]);

return (
<BudgetDetailPageContext.Provider value={values}>
Expand Down
59 changes: 59 additions & 0 deletions src/components/learner-credit-management/BudgetStatusSubtitle.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';

import PropTypes from 'prop-types';
import {
Badge, Icon, OverlayTrigger, Stack, Tooltip,
} from '@edx/paragon';
import { GroupAdd, Groups } from '@edx/paragon/icons';

import { formatDate } from './data';
import useEnterpriseGroup from './data/hooks/useEnterpriseGroup';

const BudgetStatusSubtitle = ({
badgeVariant, status, isAssignable, term, date, policy,
}) => {
const { data } = useEnterpriseGroup(policy);
const universalGroup = data?.appliesToAllContexts;
const budgetType = isAssignable ? 'Assignable' : 'Browse & Enroll';
const popoverText = universalGroup ? 'all people in your organization' : 'members added to this budget';
return (
<Stack direction="horizontal" gap={2}>
{(status !== 'Active') && (
<Badge variant={badgeVariant}>{status}</Badge>
)}
<span className="small">
{(term && date) && (
// budget expiration date
<span>{term} {formatDate(date)}</span>
)}
<span>{budgetType}</span>
{(!isAssignable) && (
<span>
<OverlayTrigger
key="budget-tooltip"
placement="top"
overlay={(
<Tooltip>
Available to {popoverText}
</Tooltip>
)}
>
<Icon size="xs" src={universalGroup ? Groups : GroupAdd} className="ml-1 d-inline-flex" />
</OverlayTrigger>
</span>
)}
</span>
</Stack>
);
};

BudgetStatusSubtitle.propTypes = {
badgeVariant: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
isAssignable: PropTypes.bool.isRequired,
term: PropTypes.string.isRequired,
date: PropTypes.string.isRequired,
policy: PropTypes.shape({}).isRequired,
};

export default BudgetStatusSubtitle;
1 change: 1 addition & 0 deletions src/components/learner-credit-management/data/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,5 @@ export const learnerCreditManagementQueryKeys = {
budgetEnterpriseOffer: (budgetId) => [...learnerCreditManagementQueryKeys.budget(budgetId), 'ecommerce'],
budgetActivity: (budgetId) => [...learnerCreditManagementQueryKeys.budget(budgetId), 'activity'],
budgetActivityOverview: (budgetId) => [...learnerCreditManagementQueryKeys.budgetActivity(budgetId), 'overview'],
group: (groupUuid) => [...learnerCreditManagementQueryKeys.all, 'group', groupUuid],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { QueryClientProvider } from '@tanstack/react-query';
import { renderHook } from '@testing-library/react-hooks';

import useEnterpriseGroup from '../useEnterpriseGroup';
import LmsApiService from '../../../../../data/services/LmsApiService';
import { queryClient } from '../../../../test/testUtils';

const wrapper = ({ children }) => (
<QueryClientProvider client={queryClient()}>{children}</QueryClientProvider>
);

const mockSubsidyAccessPolicy = {
uuid: 'test-subsidy-access-policy-uuid',
groupAssociations: ['group-uuid'],
};

const mockSubsidyAccessPolicyNoGroups = {
uuid: 'test-subsidy-access-policy-uuid',
groupAssociations: null,
};

describe('useEnterpriseGroup', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should fetch and return enterprise group', async () => {
jest.spyOn(LmsApiService, 'fetchEnterpriseGroup').mockResolvedValueOnce({
data: {
enterprise_customer: 'customer-uuid',
name: 'groupidy group',
uuid: 'group-uuid',
},
});

const { result, waitForNextUpdate } = renderHook(
() => useEnterpriseGroup(mockSubsidyAccessPolicy),
{ wrapper },
);

await waitForNextUpdate();
expect(result.current.data).toEqual(
{ enterpriseCustomer: 'customer-uuid', name: 'groupidy group', uuid: 'group-uuid' },
);
});

it('should return null if no group associations are listed', async () => {
jest.spyOn(LmsApiService, 'fetchEnterpriseGroup').mockResolvedValueOnce({
data: {
enterprise_customer: 'customer-uuid',
name: 'groupidy group',
uuid: 'group-uuid',
},
});

const { result, waitForNextUpdate } = renderHook(
() => useEnterpriseGroup(mockSubsidyAccessPolicyNoGroups),
{ wrapper },
);

await waitForNextUpdate();
expect(result.current.data).toBe(null);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useQuery } from '@tanstack/react-query';
import { camelCaseObject } from '@edx/frontend-platform/utils';

import { learnerCreditManagementQueryKeys } from '../constants';
import LmsApiService from '../../../../data/services/LmsApiService';

/**
* Retrieves a enterprise group by UUID from the API.
*
* @param {*} queryKey The queryKey from the associated `useQuery` call.
* @returns The enterprise group object
*/
const getEnterpriseGroup = async ({ subsidyAccessPolicy }) => {
if (!subsidyAccessPolicy.groupAssociations || subsidyAccessPolicy.groupAssociations.length === 0) {
return null;
}
const response = await LmsApiService.fetchEnterpriseGroup(subsidyAccessPolicy.groupAssociations[0]);
const enterpriseGroup = camelCaseObject(response.data);
return enterpriseGroup;
};

const useEnterpriseGroup = (subsidyAccessPolicy, { queryOptions } = {}) => useQuery({
queryKey: learnerCreditManagementQueryKeys.group(subsidyAccessPolicy?.uuid),
queryFn: () => getEnterpriseGroup({ subsidyAccessPolicy }),
...queryOptions,
});

export default useEnterpriseGroup;
Loading

0 comments on commit dbe78ba

Please sign in to comment.