Skip to content

Commit

Permalink
Listing for Supply chain Commodity resources Eusm flavor (#1329)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
peterMuriuki authored Feb 28, 2024
1 parent 6786643 commit 2e7ee0f
Show file tree
Hide file tree
Showing 19 changed files with 1,619 additions and 222 deletions.
2 changes: 1 addition & 1 deletion app/src/App/tests/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ describe('App - authenticated', () => {
`${LIST_COMMODITY_URL}?${viewDetailsQuery}=1`
);
wrapper.update();
expect(wrapper.find('ViewDetails')).toHaveLength(1);
expect(wrapper.find('ViewDetailsWrapper')).toHaveLength(1);

// go to new resource page
(wrapper.find('Router').prop('history') as RouteComponentProps['history']).push(
Expand Down
2 changes: 2 additions & 0 deletions app/src/configs/__mocks__/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,5 @@ export const ENABLE_QUEST = true;
export const BACKEND_ACTIVE = false;

export const ENABLE_FHIR_USER_MANAGEMENT = true;

export const COMMODITIES_LIST_RESOURCE_ID = 'ad';
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { ReactNode } from 'react';
import { Helmet } from 'react-helmet';
import { Row, Col, Button } from 'antd';
import { PageHeader, useSimpleTabularView } from '@opensrp/react-utils';
Expand All @@ -19,15 +19,16 @@ import { useTranslation } from '../../../mls';
import { TFunction } from '@opensrp/i18n';
import { RbacCheck } from '@opensrp/rbac';

export type TableData = ReturnType<typeof parseGroup>;
export type TableData = ReturnType<typeof parseGroup> & Record<string, unknown>;

export type BaseListViewProps = Pick<ViewDetailsProps, 'keyValueMapperRenderProp'> & {
export type BaseListViewProps = Partial<Pick<ViewDetailsProps, 'keyValueMapperRenderProp'>> & {
fhirBaseURL: string;
getColumns: (t: TFunction) => Column<TableData>[];
extraQueryFilters?: Record<string, string>;
createButtonLabel: string;
createButtonUrl?: string;
pageTitle: string;
viewDetailsRender?: (fhirBaseURL: string, resourceId?: string) => ReactNode;
};

/**
Expand All @@ -45,6 +46,7 @@ export const BaseListView = (props: BaseListViewProps) => {
createButtonUrl,
keyValueMapperRenderProp,
pageTitle,
viewDetailsRender,
} = props;

const { sParams } = useSearchParams();
Expand Down Expand Up @@ -106,11 +108,13 @@ export const BaseListView = (props: BaseListViewProps) => {
</div>
<TableLayout {...tableProps} />
</Col>
<ViewDetailsWrapper
resourceId={resourceId}
fhirBaseURL={fhirBaseURL}
keyValueMapperRenderProp={keyValueMapperRenderProp}
/>
{viewDetailsRender?.(fhirBaseURL, resourceId) ?? (
<ViewDetailsWrapper
resourceId={resourceId}
fhirBaseURL={fhirBaseURL}
keyValueMapperRenderProp={keyValueMapperRenderProp}
/>
)}
</Row>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const parseGroup = (obj: IGroup) => {
export interface ViewDetailsProps {
resourceId: string;
fhirBaseURL: string;
keyValueMapperRenderProp: (obj: IGroup, t: TFunction) => JSX.Element;
keyValueMapperRenderProp?: (obj: IGroup, t: TFunction) => JSX.Element;
}

export type ViewDetailsWrapperProps = Pick<
Expand Down Expand Up @@ -89,7 +89,7 @@ export const ViewDetails = (props: ViewDetailsProps) => {
}

const org = data as Group;
return keyValueMapperRenderProp(org, t);
return keyValueMapperRenderProp?.(org, t) ?? null;
};

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import React from 'react';
import { Space, Button, Divider, Dropdown, Popconfirm, MenuProps } from 'antd';
import { parseGroup } from '../../BaseComponents/GroupDetail';
import { MoreOutlined } from '@ant-design/icons';
import { ADD_EDIT_COMMODITY_URL, groupResourceType, listResourceType } from '../../../constants';
import { Link } from 'react-router-dom';
import { useTranslation } from '../../../mls';
import {
BaseListView,
BaseListViewProps,
TableData,
} from '../../BaseComponents/BaseGroupsListView';
import { TFunction } from '@opensrp/i18n';
import {
FHIRServiceClass,
SingleKeyNestedValue,
useSearchParams,
viewDetailsQuery,
} from '@opensrp/react-utils';
import { IGroup } from '@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IGroup';
import { IList } from '@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IList';
import { get } from 'lodash';
import {
getUnitMeasureCharacteristic,
supplyMgSnomedCode,
snomedCodeSystem,
} from '../../../helpers/utils';
import { useQueryClient } from 'react-query';
import {
sendErrorNotification,
sendInfoNotification,
sendSuccessNotification,
} from '@opensrp/notifications';
import { RbacCheck, useUserRole } from '@opensrp/rbac';

export interface GroupListProps {
fhirBaseURL: string;
listId: string; // commodities are added to list resource with this id
}

const keyValueDetailRender = (obj: IGroup, t: TFunction) => {
const { name, active, id, identifier } = parseGroup(obj);

const unitMeasureCharacteristic = getUnitMeasureCharacteristic(obj);

const keyValues = {
[t('Commodity Id')]: id,
[t('Identifier')]: identifier,
[t('Name')]: name,
[t('Active')]: active ? t('Active') : t('Disabled'),
[t('Unit of measure')]: get(unitMeasureCharacteristic, 'valueCodeableConcept.text'),
};

return (
<Space direction="vertical">
{Object.entries(keyValues).map(([key, value]) => {
const props = {
[key]: value,
};
return value ? (
<div key={key} data-testid="key-value">
<SingleKeyNestedValue {...props} />
</div>
) : null;
})}
</Space>
);
};

/**
* Shows the list of all group and there details
*
* @param props - GroupList component props
* @returns returns healthcare display
*/
export const DefaultCommodityList = (props: GroupListProps) => {
const { fhirBaseURL, listId } = props;

const { t } = useTranslation();
const queryClient = useQueryClient();
const { addParam } = useSearchParams();
const userRole = useUserRole();

const getItems = (record: TableData): MenuProps['items'] => {
return [
{
key: '1',
permissions: [],
label: (
<Button
data-testid="view-details"
onClick={() => addParam(viewDetailsQuery, record.id)}
type="link"
>
{t('View Details')}
</Button>
),
},
{
key: '2',
permissions: ['Group.delete'],
label: (
<Popconfirm
title={t('Are you sure you want to delete this Commodity?')}
okText={t('Yes')}
cancelText={t('No')}
onConfirm={async () => {
deleteCommodity(fhirBaseURL, record.obj, listId)
.then(() => {
queryClient.invalidateQueries([groupResourceType]).catch(() => {
sendInfoNotification(
t('Unable to refresh data at the moment, please refresh the page')
);
});
sendSuccessNotification(t('Successfully deleted commodity'));
})
.catch(() => {
sendErrorNotification(t('Deletion of commodity failed'));
});
}}
>
<Button danger type="link" style={{ color: '#' }}>
{t('Delete')}
</Button>
</Popconfirm>
),
},
]
.filter((item) => userRole.hasPermissions(item.permissions))
.map((item) => {
const { permissions, ...rest } = item;
return rest;
});
};

const getColumns = (t: TFunction) => [
{
title: t('Name'),
dataIndex: 'name' as const,
key: 'name' as const,
},
{
title: t('Active'),
dataIndex: 'active' as const,
key: 'active' as const,
render: (value: boolean) => <div>{value ? t('Active') : t('Disabled')}</div>,
},
{
title: t('type'),
dataIndex: 'type' as const,
key: 'type' as const,
},
{
title: t('Actions'),
width: '10%',
// eslint-disable-next-line react/display-name
render: (_: unknown, record: TableData) => (
<span className="d-flex align-items-center">
<RbacCheck permissions={['Group.update']}>
<>
<Link to={`${ADD_EDIT_COMMODITY_URL}/${record.id}`} className="m-0 p-1">
{t('Edit')}
</Link>
<Divider type="vertical" />
</>
</RbacCheck>
<Divider type="vertical" />
<Dropdown
menu={{ items: getItems(record) }}
placement="bottomRight"
arrow
trigger={['click']}
>
<MoreOutlined data-testid="action-dropdown" className="more-options" />
</Dropdown>
</span>
),
},
];

const baseListViewProps: BaseListViewProps = {
getColumns: getColumns,
keyValueMapperRenderProp: keyValueDetailRender,
createButtonLabel: t('Add Commodity'),
createButtonUrl: ADD_EDIT_COMMODITY_URL,
fhirBaseURL,
pageTitle: t('Commodity List'),
extraQueryFilters: {
code: `${snomedCodeSystem}|${supplyMgSnomedCode}`,
'_has:List:item:_id': listId,
},
};

return <BaseListView {...baseListViewProps} />;
};

/**
* Soft deletes a commodity resource. Sets its active to false and removes it from the
* list resource.
*
* @param fhirBaseURL - base url to fhir server
* @param obj - commodity resource to be disabled
* @param listId - id of list resource where this was referenced.
*/
export const deleteCommodity = async (fhirBaseURL: string, obj: IGroup, listId: string) => {
if (!listId) {
throw new Error('List id is not configured correctly');
}
const disabledGroup: IGroup = {
...obj,
active: false,
};
const serve = new FHIRServiceClass<IGroup>(fhirBaseURL, groupResourceType);
const listServer = new FHIRServiceClass<IList>(fhirBaseURL, listResourceType);
const list = await listServer.read(listId);
const leftEntries = (list.entry ?? []).filter((entry) => {
return entry.item.reference !== `${groupResourceType}/${obj.id}`;
});
const listPayload = {
...list,
entry: leftEntries,
};
return listServer.update(listPayload).then(() => {
return serve.update(disabledGroup);
});
};
Loading

0 comments on commit 2e7ee0f

Please sign in to comment.