Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
5 changes: 5 additions & 0 deletions doc/release/RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
* Sidebar "Jump to" navigation links to individual partner groups; clicking a link opens the corresponding section and smoothly scrolls it into view.
* Added `UrlProperty` format component and `url` format type support for partner field JSON schemas, enabling clickable links with customizable link text.
* Includes loading state with a spinner while partner field groups are being fetched.
* [OSDEV-2372](https://opensupplyhub.atlassian.net/browse/OSDEV-2372) - Implemented the Operational Details section on the Production Location page:
* Added `ClaimDataContainer` component that displays operational details submitted by management through the claim process for claimed production locations.
* The section includes a "Claimed Profile" badge, informational tooltip with a "Learn More" link, and renders claim data fields (e.g., facility description, parent company, website, contact information) as data points with contributor metadata and timestamps.
* Each data point shows the claim status, contributor name, and claim approval/creation date, maintaining consistency with other sections on the page.
* The section only appears when the production location has a non-pending claim (i.e., `claim_info` is present and its status is not `PENDING`) and contains displayable claim data.

### Release instructions
* Ensure that the following commands are included in the `post_deployment` command:
Expand Down
221 changes: 221 additions & 0 deletions src/react/src/__tests__/components/ClaimDataContainer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import React from 'react';
import renderWithProviders from '../../util/testUtils/renderWithProviders';
import ClaimDataContainer from '../../components/ProductionLocation/ClaimSection/ClaimDataContainer/ClaimDataContainer';
import { STATUS_CLAIMED } from '../../components/ProductionLocation/DataPoint/constants';


const makeClaimInfo = (overrides = {}) => ({
facility: {
website: 'https://example.com',
phone_number: '+1 234 567 8900',
minimum_order: '100 units',
average_lead_time: '30 days',
female_workers_percentage: 60,
affiliations: ['Fair Trade'],
certifications: ['ISO 9001'],
opening_date: '2010',
closing_date: null,
estimated_annual_throughput: 20000,
actual_annual_energy_consumption: null,
description: 'A sample facility.',
},
contact: {
name: 'Jane Doe',
email: '[email protected]',
},
office: {
name: 'Head Office',
address: '1 Office St',
country: 'US',
phone_number: '+1 800 000 0000',
},
contributor: { name: 'Test Contributor' },
approved_at: '2023-05-15T00:00:00Z',
created_at: '2023-01-01T00:00:00Z',
...overrides,
});

const renderComponent = (props = {}) => {
const claimInfo =
'claimInfo' in props ? props.claimInfo : makeClaimInfo();
return renderWithProviders(
<ClaimDataContainer
isClaimed={props.isClaimed ?? true}
claimInfo={claimInfo}
className={props.className}
/>,
);
};

describe('ClaimDataContainer — empty state', () => {
it('renders nothing when isClaimed is false', () => {
const { container } = renderComponent({ isClaimed: false });
expect(container.firstChild).toBeNull();
});

it('renders nothing when claimInfo is null', () => {
const { container } = renderComponent({
isClaimed: true,
claimInfo: null,
});
expect(container.firstChild).toBeNull();
});

it('renders nothing when all facility fields are empty', () => {
const { container } = renderComponent({
claimInfo: makeClaimInfo({
facility: {},
contact: null,
office: null,
}),
});
expect(container.firstChild).toBeNull();
});
});

describe('ClaimDataContainer — section header', () => {
it('renders the section title', () => {
const { getByText } = renderComponent();
expect(
getByText('Operational Details Submitted by Management'),
).toBeInTheDocument();
});

it('renders the info tooltip trigger', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('claim-data-info-tooltip')).toBeInTheDocument();
});
});

describe('ClaimDataContainer — field labels and values', () => {
it('renders all expected field labels', () => {
const { getByText } = renderComponent();

const expectedLabels = [
'Website',
'Contact Person',
'Contact Email',
'Phone Number',
'Minimum Order',
'Average Lead Time',
'Affiliations',
'Certifications/Standards/Regulations',
'Opening Date',
'Estimated Annual Throughput',
'Office Name',
'Office Address',
'Office Phone Number',
'Description',
];

expectedLabels.forEach(label => {
expect(getByText(label, { exact: true })).toBeInTheDocument();
});
});

it('renders plain field values', () => {
const claimInfo = makeClaimInfo();
const { getByText } = renderComponent({ claimInfo });

expect(
getByText(claimInfo.facility.phone_number, { exact: true }),
).toBeInTheDocument();
expect(
getByText(claimInfo.facility.minimum_order, { exact: true }),
).toBeInTheDocument();
expect(
getByText(claimInfo.facility.average_lead_time, { exact: true }),
).toBeInTheDocument();
expect(
getByText(claimInfo.facility.description, { exact: true }),
).toBeInTheDocument();
expect(
getByText('20000 kg/year', { exact: true }),
).toBeInTheDocument();
});

it('renders contact fields when contact is provided', () => {
const { getByText } = renderComponent();
expect(getByText('Jane Doe', { exact: true })).toBeInTheDocument();
expect(
getByText('[email protected]', { exact: true }),
).toBeInTheDocument();
});

it('omits contact fields when contact is null', () => {
const { queryByText } = renderComponent({
claimInfo: makeClaimInfo({ contact: null }),
});
expect(queryByText('Contact Person')).not.toBeInTheDocument();
expect(queryByText('Contact Email')).not.toBeInTheDocument();
});

it('renders office fields when office is provided', () => {
const { getByText } = renderComponent();
expect(getByText('Head Office', { exact: true })).toBeInTheDocument();
});

it('omits office fields when office is null', () => {
const { queryByText } = renderComponent({
claimInfo: makeClaimInfo({ office: null }),
});
expect(queryByText('Office Name')).not.toBeInTheDocument();
expect(queryByText('Office Address')).not.toBeInTheDocument();
expect(queryByText('Office Phone Number')).not.toBeInTheDocument();
});
});

describe('ClaimDataContainer — Claimed status chips', () => {
it('renders a Claimed chip for each displayed field', () => {
const { getAllByTestId, getAllByText } = renderComponent();
const dataPoints = getAllByTestId('data-point');
const claimedChips = getAllByText(STATUS_CLAIMED);
expect(claimedChips.length).toBe(dataPoints.length);
});
});

describe('ClaimDataContainer — contributor attribution', () => {
it('resolves contributor name from contributor.name object', () => {
const { getAllByTestId } = renderComponent({
claimInfo: makeClaimInfo({
contributor: { name: 'Acme Corp' },
}),
});
getAllByTestId('data-point-contributor').forEach(el => {
expect(el).toHaveTextContent('Acme Corp');
});
});

it('resolves contributor name from a plain string', () => {
const { getAllByTestId } = renderComponent({
claimInfo: makeClaimInfo({ contributor: 'String Contributor' }),
});
getAllByTestId('data-point-contributor').forEach(el => {
expect(el).toHaveTextContent('String Contributor');
});
});

it('prefers approved_at over created_at for the date', () => {
const { getAllByTestId } = renderComponent({
claimInfo: makeClaimInfo({
approved_at: '2023-05-15T00:00:00Z',
created_at: '2023-01-01T00:00:00Z',
}),
});
getAllByTestId('data-point-date').forEach(el => {
expect(el).toHaveTextContent('May 15, 2023');
});
});

it('falls back to created_at when approved_at is absent', () => {
const { getAllByTestId } = renderComponent({
claimInfo: makeClaimInfo({
approved_at: undefined,
created_at: '2022-11-09T00:00:00Z',
}),
});
getAllByTestId('data-point-date').forEach(el => {
expect(el).toHaveTextContent('November 9, 2022');
});
});
});
Original file line number Diff line number Diff line change
@@ -1,15 +1,116 @@
import React from 'react';
import { object, bool, shape, oneOfType, string } from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import Divider from '@material-ui/core/Divider';
import InfoOutlined from '@material-ui/icons/InfoOutlined';
import filter from 'lodash/filter';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import isString from 'lodash/isString';

import DataPoint from '../../DataPoint/DataPoint';
import { STATUS_CLAIMED } from '../../DataPoint/constants';
import IconComponent from '../../../Shared/IconComponent/IconComponent';
import LearnMoreLink from '../../Shared/LearnMoreLink/LearnMoreLink';
import BadgeClaimed from '../../../BadgeClaimed';
import {
getLocationFieldsConfig,
hasDisplayableValue,
} from '../../../FacilityDetailsClaimedInfo/utils';

import claimDataContainerStyles from './styles';

const ClaimDataContainer = ({ classes, className }) => (
<div className={`${classes.container} ${className || ''}`}>
<Typography variant="title" className={classes.title} component="h3">
Claim Data
</Typography>
</div>
);
const ClaimDataContainer = ({ classes, className, claimInfo, isClaimed }) => {
if (!isClaimed || !claimInfo) {
return null;
}

const { facility, contact, office } = claimInfo;

const contributorName = isString(claimInfo.contributor)
? claimInfo.contributor
: get(claimInfo, 'contributor.name', null);

const claimedAt =
get(claimInfo, 'approved_at') || get(claimInfo, 'created_at') || null;

const fieldsConfig = getLocationFieldsConfig(
facility || {},
contact || null,
office || null,
);

const displayableFields = filter(fieldsConfig, field =>
hasDisplayableValue(field.getValue()),
);

if (isEmpty(displayableFields)) {
return null;
}

return (
<div className={`${classes.container} ${className || ''}`}>
<div className={classes.titleRow}>
<BadgeClaimed className={classes.titleIcon} />
<Typography
variant="title"
className={classes.sectionTitle}
component="h3"
>
Operational Details Submitted by Management
</Typography>
<IconComponent
title={
<>
Data provided by the production location management
through the claim process.
<LearnMoreLink href="https://info.opensupplyhub.org/resources/claim-a-facility" />
</>
}
icon={InfoOutlined}
className={classes.infoButton}
data-testid="claim-data-info-tooltip"
/>
</div>
<div className={classes.dataPointsList}>
{displayableFields.map((field, index) => (
<React.Fragment key={field.key}>
<DataPoint
label={field.label}
value={field.getValue()}
statusLabel={STATUS_CLAIMED}
contributorName={contributorName}
date={claimedAt}
/>
{index < displayableFields.length - 1 && (
<Divider className={classes.divider} />
)}
</React.Fragment>
))}
</div>
</div>
);
};

ClaimDataContainer.propTypes = {
classes: object.isRequired,
className: string,
claimInfo: shape({
facility: object,
contact: object,
office: object,
contributor: oneOfType([string, shape({ name: string })]),
approved_at: string,
created_at: string,
}),
isClaimed: bool,
};

ClaimDataContainer.defaultProps = {
className: '',
claimInfo: null,
isClaimed: false,
};

export default withStyles(claimDataContainerStyles)(ClaimDataContainer);
Loading
Loading