Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding new budget headers #1195

Merged
merged 2 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/Sidebar/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const Sidebar = ({
}
async function fetchGroupsData() {
try {
const response = await LmsApiService.fetchEnterpriseGroup();
const response = await LmsApiService.fetchEnterpriseGroups();
// we only want to hide the feature if a customer has a group this does not
// apply to all contexts/include all users
response.data.results.forEach((group) => {
Expand Down
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') && (
kiram15 marked this conversation as resolved.
Show resolved Hide resolved
<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);
});
});
Loading
Loading