Skip to content

Commit 2e7ee0f

Browse files
authored
Listing for Supply chain Commodity resources Eusm flavor (#1329)
* Make the whole Group details section configurable * Move current commodity list implementation to DEfault * Add Eusm centric commodity list * Configurable view details section for group list view * Conditionally render list view wrt to project code * Update utils * Fix lint issues * Update snapshot tests * Show error banner if list id is not configured * Replace fallback image with skeleton image * Link material number to the identifier dataindex * Add material number to view details section * Fix test regressions * Wrap commodity edit in rbaccheck * Update mock envs to fix test regression * Update snapshot in commodity list view
1 parent 6786643 commit 2e7ee0f

File tree

19 files changed

+1619
-222
lines changed

19 files changed

+1619
-222
lines changed

app/src/App/tests/App.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ describe('App - authenticated', () => {
242242
`${LIST_COMMODITY_URL}?${viewDetailsQuery}=1`
243243
);
244244
wrapper.update();
245-
expect(wrapper.find('ViewDetails')).toHaveLength(1);
245+
expect(wrapper.find('ViewDetailsWrapper')).toHaveLength(1);
246246

247247
// go to new resource page
248248
(wrapper.find('Router').prop('history') as RouteComponentProps['history']).push(

app/src/configs/__mocks__/env.ts

+2
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,5 @@ export const ENABLE_QUEST = true;
4444
export const BACKEND_ACTIVE = false;
4545

4646
export const ENABLE_FHIR_USER_MANAGEMENT = true;
47+
48+
export const COMMODITIES_LIST_RESOURCE_ID = 'ad';

packages/fhir-group-management/src/components/BaseComponents/BaseGroupsListView/index.tsx

+12-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { ReactNode } from 'react';
22
import { Helmet } from 'react-helmet';
33
import { Row, Col, Button } from 'antd';
44
import { PageHeader, useSimpleTabularView } from '@opensrp/react-utils';
@@ -19,15 +19,16 @@ import { useTranslation } from '../../../mls';
1919
import { TFunction } from '@opensrp/i18n';
2020
import { RbacCheck } from '@opensrp/rbac';
2121

22-
export type TableData = ReturnType<typeof parseGroup>;
22+
export type TableData = ReturnType<typeof parseGroup> & Record<string, unknown>;
2323

24-
export type BaseListViewProps = Pick<ViewDetailsProps, 'keyValueMapperRenderProp'> & {
24+
export type BaseListViewProps = Partial<Pick<ViewDetailsProps, 'keyValueMapperRenderProp'>> & {
2525
fhirBaseURL: string;
2626
getColumns: (t: TFunction) => Column<TableData>[];
2727
extraQueryFilters?: Record<string, string>;
2828
createButtonLabel: string;
2929
createButtonUrl?: string;
3030
pageTitle: string;
31+
viewDetailsRender?: (fhirBaseURL: string, resourceId?: string) => ReactNode;
3132
};
3233

3334
/**
@@ -45,6 +46,7 @@ export const BaseListView = (props: BaseListViewProps) => {
4546
createButtonUrl,
4647
keyValueMapperRenderProp,
4748
pageTitle,
49+
viewDetailsRender,
4850
} = props;
4951

5052
const { sParams } = useSearchParams();
@@ -106,11 +108,13 @@ export const BaseListView = (props: BaseListViewProps) => {
106108
</div>
107109
<TableLayout {...tableProps} />
108110
</Col>
109-
<ViewDetailsWrapper
110-
resourceId={resourceId}
111-
fhirBaseURL={fhirBaseURL}
112-
keyValueMapperRenderProp={keyValueMapperRenderProp}
113-
/>
111+
{viewDetailsRender?.(fhirBaseURL, resourceId) ?? (
112+
<ViewDetailsWrapper
113+
resourceId={resourceId}
114+
fhirBaseURL={fhirBaseURL}
115+
keyValueMapperRenderProp={keyValueMapperRenderProp}
116+
/>
117+
)}
114118
</Row>
115119
</div>
116120
);

packages/fhir-group-management/src/components/BaseComponents/GroupDetail/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const parseGroup = (obj: IGroup) => {
5757
export interface ViewDetailsProps {
5858
resourceId: string;
5959
fhirBaseURL: string;
60-
keyValueMapperRenderProp: (obj: IGroup, t: TFunction) => JSX.Element;
60+
keyValueMapperRenderProp?: (obj: IGroup, t: TFunction) => JSX.Element;
6161
}
6262

6363
export type ViewDetailsWrapperProps = Pick<
@@ -89,7 +89,7 @@ export const ViewDetails = (props: ViewDetailsProps) => {
8989
}
9090

9191
const org = data as Group;
92-
return keyValueMapperRenderProp(org, t);
92+
return keyValueMapperRenderProp?.(org, t) ?? null;
9393
};
9494

9595
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import React from 'react';
2+
import { Space, Button, Divider, Dropdown, Popconfirm, MenuProps } from 'antd';
3+
import { parseGroup } from '../../BaseComponents/GroupDetail';
4+
import { MoreOutlined } from '@ant-design/icons';
5+
import { ADD_EDIT_COMMODITY_URL, groupResourceType, listResourceType } from '../../../constants';
6+
import { Link } from 'react-router-dom';
7+
import { useTranslation } from '../../../mls';
8+
import {
9+
BaseListView,
10+
BaseListViewProps,
11+
TableData,
12+
} from '../../BaseComponents/BaseGroupsListView';
13+
import { TFunction } from '@opensrp/i18n';
14+
import {
15+
FHIRServiceClass,
16+
SingleKeyNestedValue,
17+
useSearchParams,
18+
viewDetailsQuery,
19+
} from '@opensrp/react-utils';
20+
import { IGroup } from '@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IGroup';
21+
import { IList } from '@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IList';
22+
import { get } from 'lodash';
23+
import {
24+
getUnitMeasureCharacteristic,
25+
supplyMgSnomedCode,
26+
snomedCodeSystem,
27+
} from '../../../helpers/utils';
28+
import { useQueryClient } from 'react-query';
29+
import {
30+
sendErrorNotification,
31+
sendInfoNotification,
32+
sendSuccessNotification,
33+
} from '@opensrp/notifications';
34+
import { RbacCheck, useUserRole } from '@opensrp/rbac';
35+
36+
export interface GroupListProps {
37+
fhirBaseURL: string;
38+
listId: string; // commodities are added to list resource with this id
39+
}
40+
41+
const keyValueDetailRender = (obj: IGroup, t: TFunction) => {
42+
const { name, active, id, identifier } = parseGroup(obj);
43+
44+
const unitMeasureCharacteristic = getUnitMeasureCharacteristic(obj);
45+
46+
const keyValues = {
47+
[t('Commodity Id')]: id,
48+
[t('Identifier')]: identifier,
49+
[t('Name')]: name,
50+
[t('Active')]: active ? t('Active') : t('Disabled'),
51+
[t('Unit of measure')]: get(unitMeasureCharacteristic, 'valueCodeableConcept.text'),
52+
};
53+
54+
return (
55+
<Space direction="vertical">
56+
{Object.entries(keyValues).map(([key, value]) => {
57+
const props = {
58+
[key]: value,
59+
};
60+
return value ? (
61+
<div key={key} data-testid="key-value">
62+
<SingleKeyNestedValue {...props} />
63+
</div>
64+
) : null;
65+
})}
66+
</Space>
67+
);
68+
};
69+
70+
/**
71+
* Shows the list of all group and there details
72+
*
73+
* @param props - GroupList component props
74+
* @returns returns healthcare display
75+
*/
76+
export const DefaultCommodityList = (props: GroupListProps) => {
77+
const { fhirBaseURL, listId } = props;
78+
79+
const { t } = useTranslation();
80+
const queryClient = useQueryClient();
81+
const { addParam } = useSearchParams();
82+
const userRole = useUserRole();
83+
84+
const getItems = (record: TableData): MenuProps['items'] => {
85+
return [
86+
{
87+
key: '1',
88+
permissions: [],
89+
label: (
90+
<Button
91+
data-testid="view-details"
92+
onClick={() => addParam(viewDetailsQuery, record.id)}
93+
type="link"
94+
>
95+
{t('View Details')}
96+
</Button>
97+
),
98+
},
99+
{
100+
key: '2',
101+
permissions: ['Group.delete'],
102+
label: (
103+
<Popconfirm
104+
title={t('Are you sure you want to delete this Commodity?')}
105+
okText={t('Yes')}
106+
cancelText={t('No')}
107+
onConfirm={async () => {
108+
deleteCommodity(fhirBaseURL, record.obj, listId)
109+
.then(() => {
110+
queryClient.invalidateQueries([groupResourceType]).catch(() => {
111+
sendInfoNotification(
112+
t('Unable to refresh data at the moment, please refresh the page')
113+
);
114+
});
115+
sendSuccessNotification(t('Successfully deleted commodity'));
116+
})
117+
.catch(() => {
118+
sendErrorNotification(t('Deletion of commodity failed'));
119+
});
120+
}}
121+
>
122+
<Button danger type="link" style={{ color: '#' }}>
123+
{t('Delete')}
124+
</Button>
125+
</Popconfirm>
126+
),
127+
},
128+
]
129+
.filter((item) => userRole.hasPermissions(item.permissions))
130+
.map((item) => {
131+
const { permissions, ...rest } = item;
132+
return rest;
133+
});
134+
};
135+
136+
const getColumns = (t: TFunction) => [
137+
{
138+
title: t('Name'),
139+
dataIndex: 'name' as const,
140+
key: 'name' as const,
141+
},
142+
{
143+
title: t('Active'),
144+
dataIndex: 'active' as const,
145+
key: 'active' as const,
146+
render: (value: boolean) => <div>{value ? t('Active') : t('Disabled')}</div>,
147+
},
148+
{
149+
title: t('type'),
150+
dataIndex: 'type' as const,
151+
key: 'type' as const,
152+
},
153+
{
154+
title: t('Actions'),
155+
width: '10%',
156+
// eslint-disable-next-line react/display-name
157+
render: (_: unknown, record: TableData) => (
158+
<span className="d-flex align-items-center">
159+
<RbacCheck permissions={['Group.update']}>
160+
<>
161+
<Link to={`${ADD_EDIT_COMMODITY_URL}/${record.id}`} className="m-0 p-1">
162+
{t('Edit')}
163+
</Link>
164+
<Divider type="vertical" />
165+
</>
166+
</RbacCheck>
167+
<Divider type="vertical" />
168+
<Dropdown
169+
menu={{ items: getItems(record) }}
170+
placement="bottomRight"
171+
arrow
172+
trigger={['click']}
173+
>
174+
<MoreOutlined data-testid="action-dropdown" className="more-options" />
175+
</Dropdown>
176+
</span>
177+
),
178+
},
179+
];
180+
181+
const baseListViewProps: BaseListViewProps = {
182+
getColumns: getColumns,
183+
keyValueMapperRenderProp: keyValueDetailRender,
184+
createButtonLabel: t('Add Commodity'),
185+
createButtonUrl: ADD_EDIT_COMMODITY_URL,
186+
fhirBaseURL,
187+
pageTitle: t('Commodity List'),
188+
extraQueryFilters: {
189+
code: `${snomedCodeSystem}|${supplyMgSnomedCode}`,
190+
'_has:List:item:_id': listId,
191+
},
192+
};
193+
194+
return <BaseListView {...baseListViewProps} />;
195+
};
196+
197+
/**
198+
* Soft deletes a commodity resource. Sets its active to false and removes it from the
199+
* list resource.
200+
*
201+
* @param fhirBaseURL - base url to fhir server
202+
* @param obj - commodity resource to be disabled
203+
* @param listId - id of list resource where this was referenced.
204+
*/
205+
export const deleteCommodity = async (fhirBaseURL: string, obj: IGroup, listId: string) => {
206+
if (!listId) {
207+
throw new Error('List id is not configured correctly');
208+
}
209+
const disabledGroup: IGroup = {
210+
...obj,
211+
active: false,
212+
};
213+
const serve = new FHIRServiceClass<IGroup>(fhirBaseURL, groupResourceType);
214+
const listServer = new FHIRServiceClass<IList>(fhirBaseURL, listResourceType);
215+
const list = await listServer.read(listId);
216+
const leftEntries = (list.entry ?? []).filter((entry) => {
217+
return entry.item.reference !== `${groupResourceType}/${obj.id}`;
218+
});
219+
const listPayload = {
220+
...list,
221+
entry: leftEntries,
222+
};
223+
return listServer.update(listPayload).then(() => {
224+
return serve.update(disabledGroup);
225+
});
226+
};

0 commit comments

Comments
 (0)