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: add customer record card #396

Merged
merged 3 commits into from
Aug 9, 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
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useState } from 'react';
import {
Hyperlink,
Icon,
Expand All @@ -8,22 +7,19 @@ import { getConfig } from '@edx/frontend-platform';
import { Check, ContentCopy } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import ROUTES from '../../../data/constants/routes';
import { useCopyToClipboard } from '../data/utils';

const { HOME } = ROUTES.CONFIGURATION.SUB_DIRECTORY.CUSTOMERS;

export const CustomerDetailLink = ({ row }) => {
const [showToast, setShowToast] = useState(false);
const copyToClipboard = (id) => {
navigator.clipboard.writeText(id);
setShowToast(true);
};
const { showToast, copyToClipboard, setShowToast } = useCopyToClipboard();
const { ADMIN_PORTAL_BASE_URL } = getConfig();

return (
<div>
<div>
<Hyperlink
destination={`${HOME}/${row.original.slug}/view`}
destination={`${HOME}/${row.original.uuid}/view`}
key={row.original.uuid}
rel="noopener noreferrer"
variant="muted"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));

Object.assign(navigator, {
clipboard: {
writeText: () => {},
},
});
jest.mock('../../data/utils', () => ({
useCopyToClipboard: jest.fn(() => ({
showToast: true,
copyToClipboard: jest.fn(),
setShowToast: jest.fn(),
})),
}));

describe('CustomerDetails', () => {
const row = {
Expand Down Expand Up @@ -107,7 +109,7 @@ describe('CustomerDetails', () => {
<CustomerDetailLink row={row} />
</IntlProvider>,
);
expect(screen.getByRole('link', { name: 'Ash Ketchum' })).toHaveAttribute('href', '/enterprise-configuration/customers/ash-ketchum/view');
expect(screen.getByRole('link', { name: 'Ash Ketchum' })).toHaveAttribute('href', '/enterprise-configuration/customers/123456789/view');
expect(screen.getByRole('link', { name: '/ash-ketchum/ in a new tab' })).toHaveAttribute('href', 'http://www.testportal.com/ash-ketchum/admin/learners');
expect(screen.getByText('123456789')).toBeInTheDocument();
const copy = screen.getByTestId('copy');
Expand Down
95 changes: 95 additions & 0 deletions src/Configuration/Customers/CustomerDetailView/CustomerCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import PropTypes from 'prop-types';
import {
ActionRow,
Button,
Card,
Icon,
Hyperlink,
Toast,
} from '@openedx/paragon';
import { Launch, ContentCopy } from '@openedx/paragon/icons';
import { getConfig } from '@edx/frontend-platform';
import { formatDate, useCopyToClipboard } from '../data/utils';

const CustomerCard = ({ enterpriseCustomer }) => {
const { ADMIN_PORTAL_BASE_URL, LMS_BASE_URL } = getConfig();
const { showToast, copyToClipboard, setShowToast } = useCopyToClipboard();

return (
<div>
<Card variant="dark" className="mb-0">
<Card.Section
actions={(
<ActionRow>
<Button>View Details</Button>
<Button
className="text-dark-500"
as="a"
href={`${LMS_BASE_URL}/admin/enterprise/enterprisecustomer/${enterpriseCustomer.uuid}/change`}
variant="inverse-primary"
target="_blank"
rel="noopener noreferrer"
iconAfter={Launch}
>
Open in Django
</Button>
</ActionRow>
)}
>
<p className="small font-weight-bold mb-0 mt-2">
CUSTOMER RECORD
</p>
<p className="lead font-weight-bold mb-0">
{enterpriseCustomer.name}
</p>
<Hyperlink
destination={`${ADMIN_PORTAL_BASE_URL}/${enterpriseCustomer.slug}/admin/learners`}
variant="muted"
target="_blank"
showLaunchIcon
className="small mb-1"
>
/{enterpriseCustomer.slug}/
</Hyperlink>
<div
role="presentation"
className="pgn-doc__icons-table__preview-footer"
>
<p className="small mb-1">
{enterpriseCustomer.uuid}
</p>
<Icon
key="ContentCopy"
src={ContentCopy}
data-testid="copy"
onClick={() => copyToClipboard(enterpriseCustomer.uuid)}
/>
</div>
<p className="small mb-1">
Created {formatDate(enterpriseCustomer.created)} • Last modified {formatDate(enterpriseCustomer.modified)}
</p>
</Card.Section>
</Card>
<Toast
onClose={() => setShowToast(false)}
show={showToast}
delay={2000}
>
Copied to clipboard
</Toast>
</div>

);
};

CustomerCard.propTypes = {
enterpriseCustomer: PropTypes.shape({
created: PropTypes.string,
modified: PropTypes.string,
slug: PropTypes.string,
name: PropTypes.string,
uuid: PropTypes.string,
}).isRequired,
};

export default CustomerCard;
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useState, useEffect, useCallback } from 'react';
import { useParams } from 'react-router-dom';
import { logError } from '@edx/frontend-platform/logging';
import {
Breadcrumb,
Container,
Skeleton,
Stack,
} from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import CustomerCard from './CustomerCard';
import { getEnterpriseCustomer } from '../data/utils';

const CustomerViewContainer = () => {
const { id } = useParams();
const [enterpriseCustomer, setEnterpriseCustomer] = useState({});
const [isLoading, setIsLoading] = useState(true);
const intl = useIntl();

const fetchData = useCallback(
async () => {
try {
const response = await getEnterpriseCustomer({ uuid: id });
setEnterpriseCustomer(response[0]);
} catch (error) {
logError(error);
} finally {
setIsLoading(false);
}
},
[],
);

useEffect(() => {
fetchData();
}, []);

return (
<div>
{!isLoading ? (
<Container className="mt-5">
<Breadcrumb
arial-label="customer detail"
links={[
{
label: intl.formatMessage({
id: 'supportTool.customers.page.breadcrumb.customer',
defaultMessage: 'Customers',
description: 'Breadcrumb label for the customers page',
}),
href: '/enterprise-configuration/customers/',
},
{ label: enterpriseCustomer.name },
]}
/>
</Container>
) : <Skeleton />}
<Container className="mt-4">
<Stack gap={2}>
{!isLoading ? <CustomerCard enterpriseCustomer={enterpriseCustomer} /> : <Skeleton height={230} />}
</Stack>
</Container>
</div>
);
};

export default CustomerViewContainer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* eslint-disable react/prop-types */
import { screen, render } from '@testing-library/react';
import '@testing-library/jest-dom';

import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatDate } from '../../data/utils';
import CustomerCard from '../CustomerCard';

jest.mock('../../data/utils', () => ({
getEnterpriseCustomer: jest.fn(),
formatDate: jest.fn(),
useCopyToClipboard: jest.fn(() => ({
showToast: true,
copyToClipboard: jest.fn(),
setShowToast: jest.fn(),
})),
}));

const mockData = {
uuid: 'test-id',
name: 'Test Customer Name',
slug: 'customer-6',
created: '2024-07-23T20:02:57.651943Z',
modified: '2024-07-23T20:02:57.651943Z',
};

describe('CustomerCard', () => {
it('renders customer card data', () => {
formatDate.mockReturnValue('July 23, 2024');
render(
<IntlProvider locale="en">
<CustomerCard enterpriseCustomer={mockData} />
</IntlProvider>,
);
expect(screen.getByText('test-id')).toBeInTheDocument();
expect(screen.getByText('/customer-6/')).toBeInTheDocument();
expect(screen.getByText('Created July 23, 2024 • Last modified July 23, 2024')).toBeInTheDocument();
expect(screen.getByText('Test Customer Name'));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* eslint-disable react/prop-types */
import { screen, render, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';

import { IntlProvider } from '@edx/frontend-platform/i18n';
import { getEnterpriseCustomer, formatDate } from '../../data/utils';
import CustomerViewContainer from '../CustomerViewContainer';

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: () => ({ id: 'test-id' }),
}));

jest.mock('../../data/utils', () => ({
getEnterpriseCustomer: jest.fn(),
formatDate: jest.fn(),
useCopyToClipboard: jest.fn(() => ({
showToast: true,
copyToClipboard: jest.fn(),
setShowToast: jest.fn(),
})),
}));

describe('CustomerViewContainer', () => {
it('renders data', async () => {
getEnterpriseCustomer.mockReturnValue([{
uuid: 'test-id',
name: 'Test Customer Name',
slug: 'customer-6',
created: '2024-07-23T20:02:57.651943Z',
modified: '2024-07-23T20:02:57.651943Z',
}]);
formatDate.mockReturnValue('July 23, 2024');
render(
<IntlProvider locale="en">
<CustomerViewContainer />
</IntlProvider>,
);
await waitFor(() => {
expect(screen.getByText('test-id')).toBeInTheDocument();
expect(screen.getByText('/customer-6/')).toBeInTheDocument();
expect(screen.getByText('Created July 23, 2024 • Last modified July 23, 2024')).toBeInTheDocument();
const customerNameText = screen.getAllByText('Test Customer Name');
customerNameText.forEach(customerName => {
expect(customerName).toBeInTheDocument();
});
});
});
});
24 changes: 24 additions & 0 deletions src/Configuration/Customers/data/utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { useState } from 'react';
import { camelCaseObject } from '@edx/frontend-platform/utils';
import EcommerceApiService from '../../../data/services/EcommerceApiService';
import LicenseManagerApiService from '../../../data/services/LicenseManagerApiService';
import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService';
import LmsApiService from '../../../data/services/EnterpriseApiService';
import dayjs from '../../Provisioning/data/dayjs';

export const getEnterpriseOffers = async (enterpriseId) => {
const response = await EcommerceApiService.fetchEnterpriseOffers(enterpriseId);
Expand All @@ -26,3 +29,24 @@ export const getSubsidyAccessPolicies = async (enterpriseId) => {
const subsidyAccessPolicies = camelCaseObject(response.data);
return subsidyAccessPolicies;
};

export const getEnterpriseCustomer = async (options) => {
const response = await LmsApiService.fetchEnterpriseCustomerSupportTool(options);
const enterpriseCustomer = camelCaseObject(response.data);
return enterpriseCustomer;
};

export const formatDate = (date) => dayjs(date).utc().format('MMMM DD, YYYY');

export const useCopyToClipboard = (id) => {
const [showToast, setShowToast] = useState(false);
const copyToClipboard = () => {
navigator.clipboard.writeText(id);
setShowToast(true);
};
return {
showToast,
copyToClipboard,
setShowToast,
};
};
29 changes: 29 additions & 0 deletions src/Configuration/Customers/data/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
getCouponOrders,
getCustomerAgreements,
getSubsidyAccessPolicies,
getEnterpriseCustomer,
formatDate,
} from './utils';

jest.mock('@edx/frontend-platform/auth', () => ({
Expand Down Expand Up @@ -100,3 +102,30 @@ describe('getCustomerAgreements', () => {
expect(results).toEqual(agreementsResults.data);
});
});

describe('getEnterpriseCustomer', () => {
it('returns the correct data', async () => {
const enterpriseCustomer = {
data: [{
uuid: '0b466242-75ff-4c27-8237-680dac3737f7',
name: 'customer-6',
slug: 'customer-6',
active: true,
}],
};
getAuthenticatedHttpClient.mockImplementation(() => ({
get: jest.fn().mockResolvedValue(enterpriseCustomer),
}));
const results = await getEnterpriseCustomer(TEST_ENTERPRISE_UUID);
expect(results).toEqual(enterpriseCustomer.data);
});
});

describe('formatDate', () => {
it('returns the formatted date', async () => {
const date = '2024-07-23T20:02:57.651943Z';
const formattedDate = formatDate(date);
const expectedFormattedDate = 'July 23, 2024';
expect(expectedFormattedDate).toEqual(formattedDate);
});
});
Loading