Skip to content

Commit d0344b1

Browse files
[OSDEV-2372] Implement 'Operational Details Submitted by Management' section using claim info (#910)
Implements [OSDEV-2372](https://opensupplyhub.atlassian.net/browse/OSDEV-2372) Implemented the Operational Details section on the Production Location page. [OSDEV-2372]: https://opensupplyhub.atlassian.net/browse/OSDEV-2372?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent 79232a3 commit d0344b1

10 files changed

Lines changed: 566 additions & 20 deletions

File tree

doc/release/RELEASE-NOTES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
5252
* Sidebar "Jump to" navigation links to individual partner groups; clicking a link opens the corresponding section and smoothly scrolls it into view.
5353
* Added `UrlProperty` format component and `url` format type support for partner field JSON schemas, enabling clickable links with customizable link text.
5454
* Includes loading state with a spinner while partner field groups are being fetched.
55+
* [OSDEV-2372](https://opensupplyhub.atlassian.net/browse/OSDEV-2372) - Implemented the Operational Details section on the Production Location page:
56+
* Added `ClaimDataContainer` component that displays operational details submitted by management through the claim process for claimed production locations.
57+
* 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.
58+
* Each data point shows the claim status, contributor name, and claim approval/creation date, maintaining consistency with other sections on the page.
59+
* 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.
5560

5661
### Release instructions
5762
* Ensure that the following commands are included in the `post_deployment` command:
Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
import React from 'react';
2+
import { fireEvent } from '@testing-library/react';
3+
import renderWithProviders from '../../util/testUtils/renderWithProviders';
4+
import ClaimDataContainer from '../../components/ProductionLocation/ClaimSection/ClaimDataContainer/ClaimDataContainer';
5+
import { STATUS_CLAIMED } from '../../components/ProductionLocation/DataPoint/constants';
6+
7+
const makeClaimInfo = (overrides = {}) => ({
8+
facility: {
9+
website: 'https://example.com',
10+
phone_number: '+1 234 567 8900',
11+
minimum_order: '100 units',
12+
average_lead_time: '30 days',
13+
female_workers_percentage: 60,
14+
affiliations: ['Fair Trade'],
15+
certifications: ['ISO 9001'],
16+
opening_date: '2010',
17+
closing_date: null,
18+
estimated_annual_throughput: 20000,
19+
actual_annual_energy_consumption: null,
20+
description: 'A sample facility.',
21+
},
22+
contact: {
23+
name: 'Jane Doe',
24+
25+
},
26+
office: {
27+
name: 'Head Office',
28+
address: '1 Office St',
29+
country: 'US',
30+
phone_number: '+1 800 000 0000',
31+
},
32+
contributor: { name: 'Test Contributor' },
33+
approved_at: '2023-05-15T00:00:00Z',
34+
created_at: '2023-01-01T00:00:00Z',
35+
...overrides,
36+
});
37+
38+
const renderComponent = (props = {}) => {
39+
const claimInfo =
40+
'claimInfo' in props ? props.claimInfo : makeClaimInfo();
41+
return renderWithProviders(
42+
<ClaimDataContainer
43+
isClaimed={props.isClaimed ?? true}
44+
claimInfo={claimInfo}
45+
className={props.className}
46+
/>,
47+
);
48+
};
49+
50+
describe('ClaimDataContainer — empty state', () => {
51+
it('renders nothing when isClaimed is false', () => {
52+
const { container } = renderComponent({ isClaimed: false });
53+
expect(container.firstChild).toBeNull();
54+
});
55+
56+
it('renders nothing when claimInfo is null', () => {
57+
const { container } = renderComponent({
58+
isClaimed: true,
59+
claimInfo: null,
60+
});
61+
expect(container.firstChild).toBeNull();
62+
});
63+
64+
it('renders nothing when all facility fields are empty', () => {
65+
const { container } = renderComponent({
66+
claimInfo: makeClaimInfo({
67+
facility: {},
68+
contact: null,
69+
office: null,
70+
}),
71+
});
72+
expect(container.firstChild).toBeNull();
73+
});
74+
});
75+
76+
describe('ClaimDataContainer — section header', () => {
77+
it('renders the section title', () => {
78+
const { getByText } = renderComponent();
79+
expect(
80+
getByText('Operational Details Submitted by Management'),
81+
).toBeInTheDocument();
82+
});
83+
84+
it('renders the info tooltip trigger', () => {
85+
const { getByTestId } = renderComponent();
86+
expect(getByTestId('claim-data-info-tooltip')).toBeInTheDocument();
87+
});
88+
89+
it('renders the toggle switch', () => {
90+
const { getByRole } = renderComponent();
91+
expect(
92+
getByRole('checkbox', {
93+
name: /show operational details submitted by management/i,
94+
}),
95+
).toBeInTheDocument();
96+
});
97+
98+
it('shows "Close" label when content is open by default', () => {
99+
const { getByText } = renderComponent();
100+
expect(getByText('Close')).toBeInTheDocument();
101+
});
102+
103+
it('sets the operational-details id on the root element', () => {
104+
const { container } = renderComponent();
105+
expect(container.querySelector('#operational-details')).toBeInTheDocument();
106+
});
107+
});
108+
109+
describe('ClaimDataContainer — toggle switch', () => {
110+
it('content is visible by default', () => {
111+
const { getByText } = renderComponent();
112+
expect(getByText('A sample facility.')).toBeInTheDocument();
113+
});
114+
115+
it('hides content when toggled closed', () => {
116+
const { getByRole, queryByText } = renderComponent();
117+
const toggle = getByRole('checkbox', {
118+
name: /show operational details submitted by management/i,
119+
});
120+
fireEvent.click(toggle);
121+
expect(queryByText('A sample facility.')).not.toBeInTheDocument();
122+
});
123+
124+
it('shows "Open" label when content is closed', () => {
125+
const { getByRole, getByText } = renderComponent();
126+
const toggle = getByRole('checkbox', {
127+
name: /show operational details submitted by management/i,
128+
});
129+
fireEvent.click(toggle);
130+
expect(getByText('Open')).toBeInTheDocument();
131+
});
132+
133+
it('shows content again when toggled back open', () => {
134+
const { getByRole, getByText } = renderComponent();
135+
const toggle = getByRole('checkbox', {
136+
name: /show operational details submitted by management/i,
137+
});
138+
fireEvent.click(toggle);
139+
fireEvent.click(toggle);
140+
expect(getByText('A sample facility.')).toBeInTheDocument();
141+
});
142+
});
143+
144+
describe('ClaimDataContainer — field labels and values', () => {
145+
it('renders all expected field labels', () => {
146+
const { getByText } = renderComponent();
147+
148+
const expectedLabels = [
149+
'Website',
150+
'Contact Person',
151+
'Contact Email',
152+
'Phone Number',
153+
'Minimum Order',
154+
'Average Lead Time',
155+
'Affiliations',
156+
'Certifications/Standards/Regulations',
157+
'Opening Date',
158+
'Estimated Annual Throughput',
159+
'Office Name',
160+
'Office Address',
161+
'Office Phone Number',
162+
'Description',
163+
];
164+
165+
expectedLabels.forEach(label => {
166+
expect(getByText(label, { exact: true })).toBeInTheDocument();
167+
});
168+
});
169+
170+
it('renders plain field values', () => {
171+
const claimInfo = makeClaimInfo();
172+
const { getByText } = renderComponent({ claimInfo });
173+
174+
expect(
175+
getByText(claimInfo.facility.phone_number, { exact: true }),
176+
).toBeInTheDocument();
177+
expect(
178+
getByText(claimInfo.facility.minimum_order, { exact: true }),
179+
).toBeInTheDocument();
180+
expect(
181+
getByText(claimInfo.facility.average_lead_time, { exact: true }),
182+
).toBeInTheDocument();
183+
expect(
184+
getByText(claimInfo.facility.description, { exact: true }),
185+
).toBeInTheDocument();
186+
expect(
187+
getByText('20000 kg/year', { exact: true }),
188+
).toBeInTheDocument();
189+
});
190+
191+
it('renders contact fields when contact is provided', () => {
192+
const { getByText } = renderComponent();
193+
expect(getByText('Jane Doe', { exact: true })).toBeInTheDocument();
194+
expect(
195+
getByText('[email protected]', { exact: true }),
196+
).toBeInTheDocument();
197+
});
198+
199+
it('omits contact fields when contact is null', () => {
200+
const { queryByText } = renderComponent({
201+
claimInfo: makeClaimInfo({ contact: null }),
202+
});
203+
expect(queryByText('Contact Person')).not.toBeInTheDocument();
204+
expect(queryByText('Contact Email')).not.toBeInTheDocument();
205+
});
206+
207+
it('renders office fields when office is provided', () => {
208+
const { getByText } = renderComponent();
209+
expect(getByText('Head Office', { exact: true })).toBeInTheDocument();
210+
});
211+
212+
it('omits office fields when office is null', () => {
213+
const { queryByText } = renderComponent({
214+
claimInfo: makeClaimInfo({ office: null }),
215+
});
216+
expect(queryByText('Office Name')).not.toBeInTheDocument();
217+
expect(queryByText('Office Address')).not.toBeInTheDocument();
218+
expect(queryByText('Office Phone Number')).not.toBeInTheDocument();
219+
});
220+
221+
it('renders female_workers_percentage field when value is 0', () => {
222+
const { getByText } = renderComponent({
223+
claimInfo: makeClaimInfo({
224+
facility: { female_workers_percentage: 0 },
225+
contact: null,
226+
office: null,
227+
}),
228+
});
229+
expect(
230+
getByText('Percentage of female workers', { exact: true }),
231+
).toBeInTheDocument();
232+
});
233+
});
234+
235+
describe('ClaimDataContainer — field ordering', () => {
236+
it('renders fields in the order defined by FIELD_ORDER', () => {
237+
const { getAllByTestId } = renderComponent();
238+
const labels = getAllByTestId('data-point-label').map(
239+
el => el.textContent,
240+
);
241+
242+
const websiteIndex = labels.indexOf('Website');
243+
const phoneIndex = labels.indexOf('Phone Number');
244+
const officeNameIndex = labels.indexOf('Office Name');
245+
const descriptionIndex = labels.indexOf('Description');
246+
const certificationsIndex = labels.indexOf(
247+
'Certifications/Standards/Regulations',
248+
);
249+
const affiliationsIndex = labels.indexOf('Affiliations');
250+
const minimumOrderIndex = labels.indexOf('Minimum Order');
251+
252+
expect(websiteIndex).toBeLessThan(phoneIndex);
253+
expect(phoneIndex).toBeLessThan(officeNameIndex);
254+
expect(descriptionIndex).toBeLessThan(certificationsIndex);
255+
expect(certificationsIndex).toBeLessThan(affiliationsIndex);
256+
expect(affiliationsIndex).toBeLessThan(minimumOrderIndex);
257+
});
258+
});
259+
260+
describe('ClaimDataContainer — Claimed status chips', () => {
261+
it('renders a Claimed chip for each displayed field', () => {
262+
const { getAllByTestId, getAllByText } = renderComponent();
263+
const dataPoints = getAllByTestId('data-point');
264+
const claimedChips = getAllByText(STATUS_CLAIMED);
265+
expect(claimedChips.length).toBe(dataPoints.length);
266+
});
267+
});
268+
269+
describe('ClaimDataContainer — contributor attribution', () => {
270+
it('resolves contributor name from contributor.name object', () => {
271+
const { getAllByTestId } = renderComponent({
272+
claimInfo: makeClaimInfo({
273+
contributor: { name: 'Acme Corp' },
274+
}),
275+
});
276+
getAllByTestId('data-point-contributor').forEach(el => {
277+
expect(el).toHaveTextContent('Acme Corp');
278+
});
279+
});
280+
281+
it('resolves contributor name from a plain string', () => {
282+
const { getAllByTestId } = renderComponent({
283+
claimInfo: makeClaimInfo({ contributor: 'String Contributor' }),
284+
});
285+
getAllByTestId('data-point-contributor').forEach(el => {
286+
expect(el).toHaveTextContent('String Contributor');
287+
});
288+
});
289+
290+
it('prefers approved_at over created_at for the date', () => {
291+
const { getAllByTestId } = renderComponent({
292+
claimInfo: makeClaimInfo({
293+
approved_at: '2023-05-15T00:00:00Z',
294+
created_at: '2023-01-01T00:00:00Z',
295+
}),
296+
});
297+
getAllByTestId('data-point-date').forEach(el => {
298+
expect(el).toHaveTextContent('May 15, 2023');
299+
});
300+
});
301+
302+
it('falls back to created_at when approved_at is absent', () => {
303+
const { getAllByTestId } = renderComponent({
304+
claimInfo: makeClaimInfo({
305+
approved_at: undefined,
306+
created_at: '2022-11-09T00:00:00Z',
307+
}),
308+
});
309+
getAllByTestId('data-point-date').forEach(el => {
310+
expect(el).toHaveTextContent('November 9, 2022');
311+
});
312+
});
313+
});

src/react/src/components/BadgeClaimed.jsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@ import SvgIcon from '@material-ui/core/SvgIcon';
44

55
export default function BadgeClaimed({
66
color,
7-
fontSize = '24px',
87
viewBox = '0 0 16 20',
9-
overflow = 'hidden',
8+
className,
109
}) {
1110
return (
12-
<SvgIcon viewBox={viewBox} style={{ fontSize, overflow }}>
11+
<SvgIcon viewBox={viewBox} fontSize="inherit" className={className}>
1312
<path
1413
fill={color}
1514
d="M6.95 13.55L12.6 7.9L11.175 6.475L6.95 10.7L4.85 8.6L3.425 10.025L6.95 13.55ZM8 20C5.68333 19.4167 3.771 18.0873 2.263 16.012C0.754333 13.9373 0 11.6333 0 9.1V3L8 0L16 3V9.1C16 11.6333 15.246 13.9373 13.738 16.012C12.2293 18.0873 10.3167 19.4167 8 20ZM8 17.9C9.73333 17.35 11.1667 16.25 12.3 14.6C13.4333 12.95 14 11.1167 14 9.1V4.375L8 2.125L2 4.375V9.1C2 11.1167 2.56667 12.95 3.7 14.6C4.83333 16.25 6.26667 17.35 8 17.9Z"
@@ -20,8 +19,10 @@ export default function BadgeClaimed({
2019

2120
BadgeClaimed.defaultProps = {
2221
color: 'currentColor',
22+
className: undefined,
2323
};
2424

2525
BadgeClaimed.propTypes = {
2626
color: string,
27+
className: string,
2728
};

0 commit comments

Comments
 (0)