From cdb282a874ef53aa887cfcc6619cbf7bda421e93 Mon Sep 17 00:00:00 2001 From: Peter Muriuki Date: Thu, 8 Feb 2024 11:07:39 +0300 Subject: [PATCH 01/32] Update group-management dependencies --- packages/fhir-group-management/package.json | 4 +++- yarn.lock | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/fhir-group-management/package.json b/packages/fhir-group-management/package.json index 22c631218..859e438f4 100644 --- a/packages/fhir-group-management/package.json +++ b/packages/fhir-group-management/package.json @@ -31,6 +31,7 @@ "author": "OpenSRP Engineering", "license": "Apache-2.0", "dependencies": { + "@ant-design/icons": "^4.7.0", "@opensrp/notifications": "^0.0.5", "@opensrp/pkg-config": "^0.0.9", "@opensrp/rbac": "workspace:^", @@ -46,6 +47,7 @@ }, "peerDependencies": { "@opensrp/i18n": "^0.0.1", - "react": "17.0.0" + "react": "17.0.0", + "react-query": "^3.15.1" } } diff --git a/yarn.lock b/yarn.lock index d2f4d49a4..592acebed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3862,6 +3862,7 @@ __metadata: version: 0.0.0-use.local resolution: "@opensrp/fhir-group-management@workspace:packages/fhir-group-management" dependencies: + "@ant-design/icons": ^4.7.0 "@onaio/redux-reducer-registry": ^0.0.9 "@opensrp/notifications": ^0.0.5 "@opensrp/pkg-config": ^0.0.9 @@ -3875,6 +3876,7 @@ __metadata: peerDependencies: "@opensrp/i18n": ^0.0.1 react: 17.0.0 + react-query: ^3.15.1 languageName: unknown linkType: soft From 70bee89b735692ecd755447c77185c9a9322d649 Mon Sep 17 00:00:00 2001 From: Peter Muriuki Date: Thu, 8 Feb 2024 11:08:09 +0300 Subject: [PATCH 02/32] Extract general re-usable productForm --- .../src/components/ProductForm/index.tsx | 306 ++++++++++++++++++ .../src/components/ProductForm/utils.tsx | 21 ++ 2 files changed, 327 insertions(+) create mode 100644 packages/fhir-group-management/src/components/ProductForm/index.tsx create mode 100644 packages/fhir-group-management/src/components/ProductForm/utils.tsx diff --git a/packages/fhir-group-management/src/components/ProductForm/index.tsx b/packages/fhir-group-management/src/components/ProductForm/index.tsx new file mode 100644 index 000000000..5359de77d --- /dev/null +++ b/packages/fhir-group-management/src/components/ProductForm/index.tsx @@ -0,0 +1,306 @@ +import React from 'react'; +import { Select, Button, Form, Radio, Input, Space, InputNumber, Upload } from 'antd'; +import { + active, + name, + id, + identifier, + type, + unitOfMeasure, + groupResourceType, + materialNumber, + isAttractiveItem, + availability, + condition, + appropriateUsage, + accountabilityPeriod, + photoURL, +} from '../../constants'; +import { + sendSuccessNotification, + sendErrorNotification, + sendInfoNotification, +} from '@opensrp/notifications'; +import { useQueryClient, useMutation } from 'react-query'; +import { formItemLayout, tailLayout } from '@opensrp/react-utils'; +import { useHistory } from 'react-router'; +import { + generateGroupPayload, + getGroupTypeOptions, + getUnitOfMeasureOptions, + GroupFormFields, + groupSelectfilterFunction, + postPutGroup, + SelectOption, + validationRulesFactory, +} from '../CommodityAddEdit/utils'; +import { SelectProps } from 'antd/lib/select'; +import { useTranslation } from '../../mls'; +import { IGroup } from '@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IGroup'; +import { PlusOutlined } from '@ant-design/icons'; + +const { Item: FormItem } = Form; + +export interface GroupFormProps { + fhirBaseUrl: string; + initialValues: GroupFormFields; + disabled: string[]; + cancelUrl?: string; + successUrl?: string; + postSuccess?: (commodity: IGroup, edited: boolean) => Promise; +} + +const defaultProps = { + initialValues: {}, + disabled: [], +}; + + +const normFile = (e: any) => { + if (Array.isArray(e)) { + return e; + } + const fileList = e?.fileList + return fileList[0] +}; + +/** + * + * Use project code to define how the commodity view works. + * AddEditBehavior: + * - have form with everything, ingest a list of hide-able form fields + * - create different views for different projects, include one single export view that conditionally exposes the right component. + * List view + * - same: + * create default view let it be as it is, + * create another eusm view + * Add a wrapper that shows the correct container conditionally with respect to project code. + */ + +const CommodityForm = (props: GroupFormProps) => { + const { fhirBaseUrl, initialValues, disabled, cancelUrl, successUrl, postSuccess } = props; + + const queryClient = useQueryClient(); + const history = useHistory(); + const { t } = useTranslation(); + const goTo = (url = '#') => history.push(url); + + const { mutate, isLoading } = useMutation( + (values: GroupFormFields) => { + const payload = generateGroupPayload(values, initialValues); + return postPutGroup(fhirBaseUrl, payload); + }, + { + onError: (err: Error) => { + sendErrorNotification(err.message); + }, + onSuccess: async (createdGroup) => { + sendSuccessNotification(t('Commodity updated successfully')); + const isEdit = !!initialValues.id; + await postSuccess?.(createdGroup, isEdit).catch((err) => { + sendErrorNotification(err.message); + }); + queryClient.refetchQueries([groupResourceType]).catch(() => { + sendInfoNotification(t('Failed to refresh data, please refresh the page')); + }); + goTo(successUrl); + }, + } + ); + + const statusOptions = [ + { label: t('Active'), value: true }, + { label: t('Disabled'), value: false }, + ]; + + + /** options for the isAttractive form field radio buttons */ + const attractiveOptions = [ + { label: t('yes'), value: true }, + { label: t('no'), value: false }, + ]; + + const unitsOfMEasureOptions = getUnitOfMeasureOptions(); + const typeOptions = getGroupTypeOptions(); + + const validationRules = validationRulesFactory(t); + + return ( +
{ + mutate(values); + }} + initialValues={initialValues} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false} + accept="image/*" + multiple={false} + name="photoURL" + listType="picture-card" + showUploadList={false} + > + + + + + + + + + + +
+ ); +}; + +CommodityForm.defaultProps = defaultProps; + +export { CommodityForm }; diff --git a/packages/fhir-group-management/src/components/ProductForm/utils.tsx b/packages/fhir-group-management/src/components/ProductForm/utils.tsx new file mode 100644 index 000000000..b6fd1ec8e --- /dev/null +++ b/packages/fhir-group-management/src/components/ProductForm/utils.tsx @@ -0,0 +1,21 @@ +import { IGroup } from "@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IGroup"; +import { id, identifier, active, unitOfMeasure, materialNumber, isAttractiveItem, availability, condition, appropriateUsage, accountabilityPeriod, photoURL, type, name } from "fhir-group-management/src/constants"; + + +export interface GroupFormFields { + [id]?: string; + [identifier]?: string; + [active]?: boolean; + [name]?: string; + [type]?: string; + [unitOfMeasure]?: IGroup['type']; + initialObject?: IGroup; + [materialNumber]?: string; + [isAttractiveItem]?: string; + [isAttractiveItem]?: string; + [availability]?: string; + [condition]?: string; + [appropriateUsage]?: string; + [accountabilityPeriod]?: Number; + [photoURL]?: File + } \ No newline at end of file From 2d39c98dec949592849a6af216fedb607ff08305 Mon Sep 17 00:00:00 2001 From: Peter Muriuki Date: Thu, 8 Feb 2024 11:32:04 +0300 Subject: [PATCH 03/32] Define a way to dynamically provide vlaidationRules --- .../src/components/ProductForm/index.tsx | 15 +++--- .../src/components/ProductForm/types.ts | 24 +++++++++ .../src/components/ProductForm/utils.tsx | 53 ++++++++++++------- 3 files changed, 64 insertions(+), 28 deletions(-) create mode 100644 packages/fhir-group-management/src/components/ProductForm/types.ts diff --git a/packages/fhir-group-management/src/components/ProductForm/index.tsx b/packages/fhir-group-management/src/components/ProductForm/index.tsx index 5359de77d..1dc45fe2e 100644 --- a/packages/fhir-group-management/src/components/ProductForm/index.tsx +++ b/packages/fhir-group-management/src/components/ProductForm/index.tsx @@ -28,7 +28,6 @@ import { generateGroupPayload, getGroupTypeOptions, getUnitOfMeasureOptions, - GroupFormFields, groupSelectfilterFunction, postPutGroup, SelectOption, @@ -38,6 +37,8 @@ import { SelectProps } from 'antd/lib/select'; import { useTranslation } from '../../mls'; import { IGroup } from '@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IGroup'; import { PlusOutlined } from '@ant-design/icons'; +import { GroupFormFields } from './types'; +import { normalizeFileInputEvent } from './utils'; const { Item: FormItem } = Form; @@ -48,6 +49,7 @@ export interface GroupFormProps { cancelUrl?: string; successUrl?: string; postSuccess?: (commodity: IGroup, edited: boolean) => Promise; + validationsFactory: any } const defaultProps = { @@ -56,13 +58,7 @@ const defaultProps = { }; -const normFile = (e: any) => { - if (Array.isArray(e)) { - return e; - } - const fileList = e?.fileList - return fileList[0] -}; + /** * @@ -125,6 +121,7 @@ const CommodityForm = (props: GroupFormProps) => { const validationRules = validationRulesFactory(t); + return (
{ + label={t('Photo of the product (optional)')} valuePropName="fileList" getValueFromEvent={normalizeFileInputEvent}> false} accept="image/*" multiple={false} diff --git a/packages/fhir-group-management/src/components/ProductForm/types.ts b/packages/fhir-group-management/src/components/ProductForm/types.ts new file mode 100644 index 000000000..d3ff2630a --- /dev/null +++ b/packages/fhir-group-management/src/components/ProductForm/types.ts @@ -0,0 +1,24 @@ +import type { IGroup } from "@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IGroup"; +import { id, identifier, active, unitOfMeasure, materialNumber, isAttractiveItem, availability, condition, appropriateUsage, accountabilityPeriod, photoURL, type, name } from "../../constants"; +import type { TFunction } from "@opensrp/i18n"; + +export interface GroupFormFields { + [id]?: string; + [identifier]?: string; + [active]?: boolean; + [name]?: string; + [type]?: string; + [unitOfMeasure]?: IGroup['type']; + initialObject?: IGroup; + [materialNumber]?: string; + [isAttractiveItem]?: string; + [isAttractiveItem]?: string; + [availability]?: string; + [condition]?: string; + [appropriateUsage]?: string; + [accountabilityPeriod]?: Number; + [photoURL]?: File + } + + + export type ValidationRulesFactory = (t: TFunction) => {[key in keyof GroupFormFields]: Rule[]} diff --git a/packages/fhir-group-management/src/components/ProductForm/utils.tsx b/packages/fhir-group-management/src/components/ProductForm/utils.tsx index b6fd1ec8e..6ba2f812b 100644 --- a/packages/fhir-group-management/src/components/ProductForm/utils.tsx +++ b/packages/fhir-group-management/src/components/ProductForm/utils.tsx @@ -1,21 +1,36 @@ -import { IGroup } from "@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IGroup"; -import { id, identifier, active, unitOfMeasure, materialNumber, isAttractiveItem, availability, condition, appropriateUsage, accountabilityPeriod, photoURL, type, name } from "fhir-group-management/src/constants"; +import { Rule } from "antd/es/form"; +import { GroupFormFields, ValidationRulesFactory } from "./types"; +import { TFunction } from "@opensrp/i18n"; +import { id, identifier, active, unitOfMeasure, name, type, materialNumber, isAttractiveItem, availability, condition, appropriateUsage, accountabilityPeriod, photoURL } from "../../constants"; +import { TypeOfGroup, UnitOfMeasure } from "../CommodityAddEdit/utils"; +/** extract file from an input event */ +export const normalizeFileInputEvent = (e: any) => { + if (Array.isArray(e)) { + return e; + } + const fileList = e?.fileList + return fileList[0] + }; -export interface GroupFormFields { - [id]?: string; - [identifier]?: string; - [active]?: boolean; - [name]?: string; - [type]?: string; - [unitOfMeasure]?: IGroup['type']; - initialObject?: IGroup; - [materialNumber]?: string; - [isAttractiveItem]?: string; - [isAttractiveItem]?: string; - [availability]?: string; - [condition]?: string; - [appropriateUsage]?: string; - [accountabilityPeriod]?: Number; - [photoURL]?: File - } \ No newline at end of file + /** factory to create default validation rules */ +export function defaultValidationRulesFactory(t: TFunction) { + return { + + [id]: [{ type: 'string' }] as Rule[], + [identifier]: [{ type: 'string' }] as Rule[], + [name]: [ + { type: 'string', message: t('Must be a valid string') }, + ] as Rule[], + [active]: [{ type: 'boolean' },] as Rule[], + [type]: [{ type: 'enum', enum: Object.values(TypeOfGroup) }] as Rule[], + [unitOfMeasure]: [{ type: 'enum', enum: Object.values(UnitOfMeasure)}] as Rule[], + [materialNumber]: [{ type: 'string' }] as Rule[], + [isAttractiveItem]: [{ type: 'boolean' }] as Rule[], + [availability]: [{ type: 'string' }] as Rule[], + [condition]: [{ type: 'string' }] as Rule[], + [appropriateUsage]: [{ type: 'string' }] as Rule[], + [accountabilityPeriod]: [{ type: 'number' }] as Rule[], + [photoURL]: [{type: "object"}] as Rule[] + } +} \ No newline at end of file From 033c21b7eb931a3c7be181e69379bf84b83752ac Mon Sep 17 00:00:00 2001 From: Peter Muriuki Date: Thu, 8 Feb 2024 11:39:40 +0300 Subject: [PATCH 04/32] Make it possible to configure hiddenfilds and validationRUles --- .../src/components/ProductForm/index.tsx | 38 ++++++++++++------- .../src/components/ProductForm/types.ts | 1 + 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/packages/fhir-group-management/src/components/ProductForm/index.tsx b/packages/fhir-group-management/src/components/ProductForm/index.tsx index 1dc45fe2e..38b2a0e8a 100644 --- a/packages/fhir-group-management/src/components/ProductForm/index.tsx +++ b/packages/fhir-group-management/src/components/ProductForm/index.tsx @@ -31,25 +31,26 @@ import { groupSelectfilterFunction, postPutGroup, SelectOption, - validationRulesFactory, } from '../CommodityAddEdit/utils'; import { SelectProps } from 'antd/lib/select'; import { useTranslation } from '../../mls'; import { IGroup } from '@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IGroup'; import { PlusOutlined } from '@ant-design/icons'; -import { GroupFormFields } from './types'; +import { GroupFormFields, ValidationRulesFactory } from './types'; import { normalizeFileInputEvent } from './utils'; +import { Group } from 'fhir-group-management/src/types'; const { Item: FormItem } = Form; export interface GroupFormProps { fhirBaseUrl: string; initialValues: GroupFormFields; - disabled: string[]; + disabled: (keyof GroupFormFields)[]; + hidden: (keyof GroupFormFields)[]; cancelUrl?: string; successUrl?: string; postSuccess?: (commodity: IGroup, edited: boolean) => Promise; - validationsFactory: any + validationRulesFactory: ValidationRulesFactory } const defaultProps = { @@ -74,7 +75,7 @@ const defaultProps = { */ const CommodityForm = (props: GroupFormProps) => { - const { fhirBaseUrl, initialValues, disabled, cancelUrl, successUrl, postSuccess } = props; + const { fhirBaseUrl, initialValues, disabled, hidden, cancelUrl, successUrl, postSuccess, validationRulesFactory } = props; const queryClient = useQueryClient(); const history = useHistory(); @@ -141,6 +142,7 @@ const CommodityForm = (props: GroupFormProps) => { + diff --git a/packages/fhir-group-management/src/components/ProductForm/types.ts b/packages/fhir-group-management/src/components/ProductForm/types.ts index d3ff2630a..5c39d714b 100644 --- a/packages/fhir-group-management/src/components/ProductForm/types.ts +++ b/packages/fhir-group-management/src/components/ProductForm/types.ts @@ -1,6 +1,7 @@ import type { IGroup } from "@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IGroup"; import { id, identifier, active, unitOfMeasure, materialNumber, isAttractiveItem, availability, condition, appropriateUsage, accountabilityPeriod, photoURL, type, name } from "../../constants"; import type { TFunction } from "@opensrp/i18n"; +import { Rule } from "antd/es/form"; export interface GroupFormFields { [id]?: string; From 3619bbc9c23e944632b2cac8f0dfbcd7c0cfbb49 Mon Sep 17 00:00:00 2001 From: Peter Muriuki Date: Fri, 9 Feb 2024 10:56:56 +0300 Subject: [PATCH 05/32] Make the whole Group details section configurable --- .../BaseComponents/BaseGroupsListView/index.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/fhir-group-management/src/components/BaseComponents/BaseGroupsListView/index.tsx b/packages/fhir-group-management/src/components/BaseComponents/BaseGroupsListView/index.tsx index 8470b875d..78a6fe11e 100644 --- a/packages/fhir-group-management/src/components/BaseComponents/BaseGroupsListView/index.tsx +++ b/packages/fhir-group-management/src/components/BaseComponents/BaseGroupsListView/index.tsx @@ -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'; @@ -28,6 +28,7 @@ export type BaseListViewProps = Pick ReactNode }; /** @@ -45,6 +46,7 @@ export const BaseListView = (props: BaseListViewProps) => { createButtonUrl, keyValueMapperRenderProp, pageTitle, + viewDetailsRender } = props; const { sParams } = useSearchParams(); @@ -106,11 +108,11 @@ export const BaseListView = (props: BaseListViewProps) => { - + />} ); From 5fb9b481eeb8aea7378ced5df9e3f2cf48cfd542 Mon Sep 17 00:00:00 2001 From: Peter Muriuki Date: Mon, 12 Feb 2024 09:50:33 +0300 Subject: [PATCH 06/32] Move current commodity list implementation to DEfault --- .../components/CommodityList/Default/List.tsx | 217 +++++++ .../CommodityList/Default/tests/fixtures.ts | 538 ++++++++++++++++++ .../CommodityList/Default/tests/list.test.tsx | 288 ++++++++++ 3 files changed, 1043 insertions(+) create mode 100644 packages/fhir-group-management/src/components/CommodityList/Default/List.tsx create mode 100644 packages/fhir-group-management/src/components/CommodityList/Default/tests/fixtures.ts create mode 100644 packages/fhir-group-management/src/components/CommodityList/Default/tests/list.test.tsx diff --git a/packages/fhir-group-management/src/components/CommodityList/Default/List.tsx b/packages/fhir-group-management/src/components/CommodityList/Default/List.tsx new file mode 100644 index 000000000..caed56c99 --- /dev/null +++ b/packages/fhir-group-management/src/components/CommodityList/Default/List.tsx @@ -0,0 +1,217 @@ +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 { 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 ( + + {Object.entries(keyValues).map(([key, value]) => { + const props = { + [key]: value, + }; + return value ? ( +
+ +
+ ) : null; + })} +
+ ); +}; + +/** + * 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: ( + + ), + }, + { + key: '2', + permissions: ['Group.delete'], + label: ( + { + 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')); + }); + }} + > + + + ), + }, + ] + .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) =>
{value ? t('Active') : t('Disabled')}
, + }, + { + 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) => ( + + + {t('Edit')} + + + + + + + ), + }, + ]; + + 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 ; +}; + +/** + * 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(fhirBaseURL, groupResourceType); + const listServer = new FHIRServiceClass(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); + }); +}; diff --git a/packages/fhir-group-management/src/components/CommodityList/Default/tests/fixtures.ts b/packages/fhir-group-management/src/components/CommodityList/Default/tests/fixtures.ts new file mode 100644 index 000000000..e2937e3b2 --- /dev/null +++ b/packages/fhir-group-management/src/components/CommodityList/Default/tests/fixtures.ts @@ -0,0 +1,538 @@ +export const firstTwentyCommodities = { + resourceType: 'Bundle', + id: '9cdfbf16-0b4f-4b2d-9e39-10bb78c950f4', + meta: { + lastUpdated: '2023-03-09T13:03:23.084+00:00', + }, + type: 'searchset', + total: 41, + link: [ + { + relation: 'self', + url: 'https://fhir.labs.smartregister.org/fhir/Group/_search?_count=50&_elements=name%2Cid&code=http%3A%2F%2Fsnomed.info%2Fsct%7C386452003', + }, + ], + entry: [ + { + fullUrl: + 'https://fhir.labs.smartregister.org/fhir/Group/6f3980e0-d1d6-4a7a-a950-939f3ca7b301', + resource: { + resourceType: 'Group', + id: '6f3980e0-d1d6-4a7a-a950-939f3ca7b301', + meta: { + versionId: '2', + lastUpdated: '2023-02-05T18:33:19.727+00:00', + source: '#94a8e26dbf91711a', + tag: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue', + code: 'SUBSETTED', + display: 'Resource encoded in summary mode', + }, + ], + }, + name: 'Albendazole 400mg Tablets', + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: + 'https://fhir.labs.smartregister.org/fhir/Group/e50eb835-7827-4001-b233-e1dda721d4e8', + resource: { + resourceType: 'Group', + id: 'e50eb835-7827-4001-b233-e1dda721d4e8', + meta: { + versionId: '2', + lastUpdated: '2023-02-05T18:33:38.412+00:00', + source: '#486b8b087008c53d', + tag: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue', + code: 'SUBSETTED', + display: 'Resource encoded in summary mode', + }, + ], + }, + name: 'Amoxicillin 250mg Tablets', + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: + 'https://fhir.labs.smartregister.org/fhir/Group/90b10fdb-592c-47b6-a265-c8806a15d77c', + resource: { + resourceType: 'Group', + id: '90b10fdb-592c-47b6-a265-c8806a15d77c', + meta: { + versionId: '1', + lastUpdated: '2022-08-16T09:07:53.596+00:00', + source: '#6436f8c5f4d5864a', + tag: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue', + code: 'SUBSETTED', + display: 'Resource encoded in summary mode', + }, + ], + }, + name: 'Artemether 20mg + Lumefatrine 120mg (1x6) Tablets', + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: + 'https://fhir.labs.smartregister.org/fhir/Group/dde1cd4f-bef4-4d2b-ad1b-f63b639ed254', + resource: { + resourceType: 'Group', + id: 'dde1cd4f-bef4-4d2b-ad1b-f63b639ed254', + meta: { + versionId: '1', + lastUpdated: '2022-08-16T09:09:10.103+00:00', + source: '#9393ec2c3c90dbc6', + tag: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue', + code: 'SUBSETTED', + display: 'Resource encoded in summary mode', + }, + ], + }, + name: 'Artemether 20mg + Lumefatrine 120mg (2x6) Tablets', + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: + 'https://fhir.labs.smartregister.org/fhir/Group/592181bc-0a68-47bc-8275-ac853bba1b09', + resource: { + resourceType: 'Group', + id: '592181bc-0a68-47bc-8275-ac853bba1b09', + meta: { + versionId: '1', + lastUpdated: '2022-08-16T09:09:26.299+00:00', + source: '#e3e8678f84456c49', + tag: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue', + code: 'SUBSETTED', + display: 'Resource encoded in summary mode', + }, + ], + }, + name: 'Artesunate 100mg Suppository Strips', + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: + 'https://fhir.labs.smartregister.org/fhir/Group/b339a63b-84db-45e8-b357-7fcce3bddc34', + resource: { + resourceType: 'Group', + id: 'b339a63b-84db-45e8-b357-7fcce3bddc34', + meta: { + versionId: '1', + lastUpdated: '2022-08-16T09:09:49.669+00:00', + source: '#28e6aad3168c2aab', + tag: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue', + code: 'SUBSETTED', + display: 'Resource encoded in summary mode', + }, + ], + }, + name: 'AS (25mg) + AQ (67.5mg) ( 2-11months) Tablets', + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: + 'https://fhir.labs.smartregister.org/fhir/Group/7c5c3eb6-0382-4c7b-8c2d-3abfb31d29f4', + resource: { + resourceType: 'Group', + id: '7c5c3eb6-0382-4c7b-8c2d-3abfb31d29f4', + meta: { + versionId: '1', + lastUpdated: '2022-08-16T09:11:04.405+00:00', + source: '#fdf04150dcf9dc32', + tag: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue', + code: 'SUBSETTED', + display: 'Resource encoded in summary mode', + }, + ], + }, + name: 'AS (50mg) + AQ (135mg) ( 1-5years) Tablets', + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: + 'https://fhir.labs.smartregister.org/fhir/Group/9aa4d38c-1c8c-11ed-861d-0242ac120002', + resource: { + resourceType: 'Group', + id: '9aa4d38c-1c8c-11ed-861d-0242ac120002', + meta: { + versionId: '1', + lastUpdated: '2022-08-16T09:11:25.174+00:00', + source: '#9e68399ed24c29c9', + tag: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue', + code: 'SUBSETTED', + display: 'Resource encoded in summary mode', + }, + ], + }, + name: 'Dispensing Bags for Tablets (s)', + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: + 'https://fhir.labs.smartregister.org/fhir/Group/3e5529c8-1c8d-11ed-861d-0242ac120002', + resource: { + resourceType: 'Group', + id: '3e5529c8-1c8d-11ed-861d-0242ac120002', + meta: { + versionId: '1', + lastUpdated: '2022-08-16T09:11:42.203+00:00', + source: '#ed5fd0ce92608473', + tag: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue', + code: 'SUBSETTED', + display: 'Resource encoded in summary mode', + }, + ], + }, + name: 'Dispensing Envelopes', + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: + 'https://fhir.labs.smartregister.org/fhir/Group/1a06dc73-575f-4cff-9424-c95605ba7c30', + resource: { + resourceType: 'Group', + id: '1a06dc73-575f-4cff-9424-c95605ba7c30', + meta: { + versionId: '1', + lastUpdated: '2022-08-16T09:11:57.209+00:00', + source: '#d5f7c8c9711078d3', + tag: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue', + code: 'SUBSETTED', + display: 'Resource encoded in summary mode', + }, + ], + }, + name: 'Disposable Gloves', + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: + 'https://fhir.labs.smartregister.org/fhir/Group/961eb18c-1c8e-11ed-861d-0242ac120002', + resource: { + resourceType: 'Group', + id: '961eb18c-1c8e-11ed-861d-0242ac120002', + meta: { + versionId: '1', + lastUpdated: '2022-08-16T09:12:22.358+00:00', + source: '#a1c46f8cde629cbc', + tag: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue', + code: 'SUBSETTED', + display: 'Resource encoded in summary mode', + }, + ], + }, + name: 'Examination Gloves (Nitrile) Large', + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: + 'https://fhir.labs.smartregister.org/fhir/Group/23c0fed8-1c8e-11ed-861d-0242ac120002', + resource: { + resourceType: 'Group', + id: '23c0fed8-1c8e-11ed-861d-0242ac120002', + meta: { + versionId: '1', + lastUpdated: '2022-08-16T09:12:38.605+00:00', + source: '#bddaf6d5d84e82c9', + tag: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue', + code: 'SUBSETTED', + display: 'Resource encoded in summary mode', + }, + ], + }, + name: 'Examination Gloves (Nitrile) Medium', + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: + 'https://fhir.labs.smartregister.org/fhir/Group/8e6fc3e6-1c8d-11ed-861d-0242ac120002', + resource: { + resourceType: 'Group', + id: '8e6fc3e6-1c8d-11ed-861d-0242ac120002', + meta: { + versionId: '1', + lastUpdated: '2022-08-16T09:12:55.303+00:00', + source: '#9edc513b5e780319', + tag: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue', + code: 'SUBSETTED', + display: 'Resource encoded in summary mode', + }, + ], + }, + name: 'Examination Gloves (Nitrile) Small', + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: + 'https://fhir.labs.smartregister.org/fhir/Group/75a7f6ec-1c8f-11ed-861d-0242ac120002', + resource: { + resourceType: 'Group', + id: '75a7f6ec-1c8f-11ed-861d-0242ac120002', + meta: { + versionId: '1', + lastUpdated: '2022-08-16T09:13:18.233+00:00', + source: '#839cc2f03dafdef9', + tag: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue', + code: 'SUBSETTED', + display: 'Resource encoded in summary mode', + }, + ], + }, + name: 'Face Mask, Surgical', + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: + 'https://fhir.labs.smartregister.org/fhir/Group/3b603f72-22b1-40f9-b2e0-5e9c3df7003f', + resource: { + resourceType: 'Group', + id: '3b603f72-22b1-40f9-b2e0-5e9c3df7003f', + meta: { + versionId: '1', + lastUpdated: '2022-08-16T09:13:39.148+00:00', + source: '#33d624162b79e9c5', + tag: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue', + code: 'SUBSETTED', + display: 'Resource encoded in summary mode', + }, + ], + }, + name: 'Face Shield (Flexible, Disposable)', + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: + 'https://fhir.labs.smartregister.org/fhir/Group/3af23539-850a-44ed-8fb1-d4999e2145ff', + resource: { + resourceType: 'Group', + id: '3af23539-850a-44ed-8fb1-d4999e2145ff', + meta: { + versionId: '2', + lastUpdated: '2022-08-16T09:15:52.994+00:00', + source: '#37d9b080de8aa1a6', + tag: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue', + code: 'SUBSETTED', + display: 'Resource encoded in summary mode', + }, + ], + }, + name: 'MNP', + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: + 'https://fhir.labs.smartregister.org/fhir/Group/21fc9958-1c8f-11ed-861d-0242ac120002', + resource: { + resourceType: 'Group', + id: '21fc9958-1c8f-11ed-861d-0242ac120002', + meta: { + versionId: '1', + lastUpdated: '2022-08-16T09:14:13.494+00:00', + source: '#80cdd5f88cbee79e', + tag: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue', + code: 'SUBSETTED', + display: 'Resource encoded in summary mode', + }, + ], + }, + name: 'Goggles', + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: + 'https://fhir.labs.smartregister.org/fhir/Group/6e7d2b70-1c90-11ed-861d-0242ac120002', + resource: { + resourceType: 'Group', + id: '6e7d2b70-1c90-11ed-861d-0242ac120002', + meta: { + versionId: '1', + lastUpdated: '2022-08-16T09:14:29.274+00:00', + source: '#e3c0c78873252c3b', + tag: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue', + code: 'SUBSETTED', + display: 'Resource encoded in summary mode', + }, + ], + }, + name: 'Hand sanitizer gel 250ml w/ pump', + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: + 'https://fhir.labs.smartregister.org/fhir/Group/951da426-1506-4cab-b03e-5583bdf0ca76', + resource: { + resourceType: 'Group', + id: '951da426-1506-4cab-b03e-5583bdf0ca76', + meta: { + versionId: '1', + lastUpdated: '2022-08-16T09:14:48.719+00:00', + source: '#e5bd2c1cd7c9736a', + tag: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue', + code: 'SUBSETTED', + display: 'Resource encoded in summary mode', + }, + ], + }, + name: 'Male Condoms', + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: + 'https://fhir.labs.smartregister.org/fhir/Group/f2734756-a6bb-4e90-bbc6-1c34f51d3d5c', + resource: { + resourceType: 'Group', + id: 'f2734756-a6bb-4e90-bbc6-1c34f51d3d5c', + meta: { + versionId: '1', + lastUpdated: '2022-08-16T09:15:13.553+00:00', + source: '#3e76e58e7815c00b', + tag: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue', + code: 'SUBSETTED', + display: 'Resource encoded in summary mode', + }, + ], + }, + name: 'Microgynon', + }, + search: { + mode: 'match', + }, + }, + ], +}; + +export const listResource = { + resourceType: 'List', + id: 'ea15c35a-8e8c-47ce-8122-c347cefa1b4a', + meta: { + versionId: '2', + lastUpdated: '2023-01-29T23:23:38.919+00:00', + source: '#615efee7bf681ece', + }, + identifier: [ + { + use: 'official', + value: 'ea15c35a-8e8c-47ce-8122-c347cefa1b4a', + }, + ], + status: 'current', + mode: 'working', + title: 'Supply Chain commodities', + code: { + coding: [ + { + system: 'http://ona.io', + code: 'supply-chain', + display: 'Supply Chain Commodity', + }, + ], + text: 'Supply Chain Commodity', + }, + entry: [ + { + item: { + reference: 'Group/6f3980e0-d1d6-4a7a-a950-939f3ca7b301', + }, + }, + { + item: { + reference: 'Group/e50eb835-7827-4001-b233-e1dda721d4e8', + }, + }, + ], +}; diff --git a/packages/fhir-group-management/src/components/CommodityList/Default/tests/list.test.tsx b/packages/fhir-group-management/src/components/CommodityList/Default/tests/list.test.tsx new file mode 100644 index 000000000..b937f834d --- /dev/null +++ b/packages/fhir-group-management/src/components/CommodityList/Default/tests/list.test.tsx @@ -0,0 +1,288 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { CommodityList } from '../..'; +import React from 'react'; +import { store } from '@opensrp/store'; +import { createMemoryHistory } from 'history'; +import { Route, Router, Switch } from 'react-router'; +import { Provider } from 'react-redux'; +import { authenticateUser } from '@onaio/session-reducer'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import nock from 'nock'; +import { waitForElementToBeRemoved } from '@testing-library/dom'; +import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { groupResourceType, listResourceType, LIST_COMMODITY_URL } from '../../../../constants'; +import { firstTwentyCommodities, listResource } from './fixtures'; +import { RoleContext } from '@opensrp/rbac'; +import { superUserRole } from '@opensrp/react-utils'; + +jest.mock('fhirclient', () => { + return jest.requireActual('fhirclient/lib/entry/browser'); +}); + +jest.setTimeout(10000); + +jest.mock('@opensrp/react-utils', () => { + const actual = jest.requireActual('@opensrp/react-utils'); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const SearchForm = (props: any) => { + const { onChangeHandler } = props; + return ( +
+ +
+ ); + }; + return { + ...actual, + SearchForm, + }; +}); + +nock.disableNetConnect(); + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + cacheTime: 0, + }, + }, +}); + +const listResId = listResource.id; + +const props = { + fhirBaseURL: 'http://test.server.org', + listId: listResId, +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const AppWrapper = (props: any) => { + return ( + + + + + + {(routeProps) => } + + + {(routeProps) => } + + + + + + ); +}; + +beforeAll(() => { + store.dispatch( + authenticateUser( + true, + { + email: 'bob@example.com', + name: 'Bobbie', + username: 'RobertBaratheon', + }, + { api_token: 'hunter2', oAuth2Data: { access_token: 'sometoken', state: 'abcde' } } + ) + ); +}); + +afterEach(() => { + nock.cleanAll(); + cleanup(); +}); + +afterAll(() => { + nock.enableNetConnect(); +}); + +test('renders correctly when listing resources', async () => { + const history = createMemoryHistory(); + history.push(LIST_COMMODITY_URL); + + nock(props.fhirBaseURL) + .get(`/${groupResourceType}/_search`) + .query({ + _total: 'accurate', + _getpagesoffset: 0, + _count: 20, + code: 'http://snomed.info/sct|386452003', + '_has:List:item:_id': listResId, + }) + .reply(200, firstTwentyCommodities); + + render( + + + + ); + + await waitForElementToBeRemoved(document.querySelector('.ant-spin')); + + expect(document.querySelector('title')).toMatchInlineSnapshot(` + + Commodity List + + `); + + expect(document.querySelector('.page-header')).toMatchSnapshot('Header title'); + + document.querySelectorAll('tr').forEach((tr, idx) => { + tr.querySelectorAll('td').forEach((td) => { + expect(td).toMatchSnapshot(`table row ${idx} page 1`); + }); + }); + + // view details + nock(props.fhirBaseURL) + .get(`/${groupResourceType}/6f3980e0-d1d6-4a7a-a950-939f3ca7b301`) + .reply(200, firstTwentyCommodities.entry[1].resource); + + // target the initial row view details + const dropdown = document.querySelector('tbody tr:nth-child(1) [data-testid="action-dropdown"]'); + fireEvent.click(dropdown!); + + const viewDetailsLink = screen.getByText(/View Details/); + expect(viewDetailsLink).toMatchInlineSnapshot(` + + View Details + + `); + fireEvent.click(viewDetailsLink); + expect(history.location.search).toEqual('?viewDetails=6f3980e0-d1d6-4a7a-a950-939f3ca7b301'); + + await waitForElementToBeRemoved(document.querySelector('.ant-spin')); + + // see view details contents + const keyValuePairs = document.querySelectorAll( + 'div[data-testid="key-value"] .singleKeyValue-pair' + ); + keyValuePairs.forEach((pair) => { + expect(pair).toMatchSnapshot(); + }); + + // close view details + const closeButton = document.querySelector('[data-testid="close-button"]'); + fireEvent.click(closeButton!); + + expect(history.location.pathname).toEqual('/commodity/list'); + expect(nock.isDone()).toBeTruthy(); +}); + +test('Can delete commodity', async () => { + const history = createMemoryHistory(); + history.push(LIST_COMMODITY_URL); + + nock(props.fhirBaseURL) + .get(`/${groupResourceType}/_search`) + .query({ + _total: 'accurate', + _getpagesoffset: 0, + _count: 20, + code: 'http://snomed.info/sct|386452003', + '_has:List:item:_id': listResId, + }) + .reply(200, firstTwentyCommodities) + .persist(); + + const { queryByRole, queryByText } = render( + + + + ); + + await waitForElementToBeRemoved(document.querySelector('.ant-spin')); + + const firstMoreOptions = document.querySelectorAll('.more-options')[0]; + + fireEvent.click(firstMoreOptions); + const editedGroup = { + ...firstTwentyCommodities.entry[0].resource, + active: false, + }; + const editedListResource = { + ...listResource, + entry: [listResource.entry[1]], + }; + + nock(props.fhirBaseURL) + .put(`/${groupResourceType}/${editedGroup.id}`, editedGroup) + .reply(201, {}); + nock(props.fhirBaseURL).get(`/${listResourceType}/${listResId}`).reply(200, listResource); + nock(props.fhirBaseURL) + .put(`/${listResourceType}/${editedListResource.id}`, editedListResource) + .reply(200, {}); + + const deleteBtn = queryByRole('button', { name: 'Delete' }) as Element; + fireEvent.click(deleteBtn); + const promptText = queryByText(/Are you sure you want to delete this Commodity\?/); + expect(promptText).toBeInTheDocument(); + // simulate yes + const yesBtn = queryByRole('button', { name: 'Yes' }) as Element; + fireEvent.click(yesBtn); + + await waitFor(() => { + expect(screen.queryByText(/Successfully deleted commodity/)).toBeInTheDocument(); + }); + + fireEvent.click(document.querySelector('.ant-notification-notice-close') as Element); +}); + +test('Failed commodity deletion', async () => { + const history = createMemoryHistory(); + history.push(LIST_COMMODITY_URL); + + nock(props.fhirBaseURL) + .get(`/${groupResourceType}/_search`) + .query({ + _total: 'accurate', + _getpagesoffset: 0, + _count: 20, + code: 'http://snomed.info/sct|386452003', + '_has:List:item:_id': listResId, + }) + .reply(200, firstTwentyCommodities); + const { queryByRole, queryByText } = render( + + + + ); + + await waitForElementToBeRemoved(document.querySelector('.ant-spin')); + + const firstMoreOptions = document.querySelectorAll('.more-options')[0]; + + fireEvent.click(firstMoreOptions); + const editedGroup = { + ...firstTwentyCommodities.entry[0].resource, + active: false, + }; + const editedListResource = { + ...listResource, + entry: [listResource.entry[1]], + }; + + nock(props.fhirBaseURL) + .put(`/${groupResourceType}/${editedGroup.id}`, editedGroup) + .reply(201, {}); + nock(props.fhirBaseURL).get(`/${listResourceType}/${listResId}`).reply(200, listResource); + nock(props.fhirBaseURL) + .put(`/${listResourceType}/${editedListResource.id}`, editedListResource) + .reply(500, 'server down'); + + const deleteBtn = queryByRole('button', { name: 'Delete' }) as Element; + fireEvent.click(deleteBtn); + const promptText = queryByText(/Are you sure you want to delete this Commodity\?/); + expect(promptText).toBeInTheDocument(); + // simulate yes + const yesBtn = queryByRole('button', { name: 'Yes' }) as Element; + fireEvent.click(yesBtn); + + await waitFor(() => { + expect(screen.queryByText(/Deletion of commodity failed/)).toBeInTheDocument(); + }); +}); From dc2ab4b46c7deff72fd65f6849a495f71431a8a2 Mon Sep 17 00:00:00 2001 From: Peter Muriuki Date: Mon, 12 Feb 2024 09:50:56 +0300 Subject: [PATCH 07/32] Add Eusm centric commodity list --- .../components/CommodityList/Eusm/List.tsx | 125 ++++++++ .../CommodityList/Eusm/ViewDetails.tsx | 178 ++++++++++++ .../tests/__snapshots__/list.test.tsx.snap | 268 ++++++++++++++++++ .../CommodityList/Eusm/tests/fixtures.ts | 196 +++++++++++++ .../CommodityList/Eusm/tests/list.test.tsx | 174 ++++++++++++ .../Eusm/tests/viewDetails.test.tsx | 118 ++++++++ 6 files changed, 1059 insertions(+) create mode 100644 packages/fhir-group-management/src/components/CommodityList/Eusm/List.tsx create mode 100644 packages/fhir-group-management/src/components/CommodityList/Eusm/ViewDetails.tsx create mode 100644 packages/fhir-group-management/src/components/CommodityList/Eusm/tests/__snapshots__/list.test.tsx.snap create mode 100644 packages/fhir-group-management/src/components/CommodityList/Eusm/tests/fixtures.ts create mode 100644 packages/fhir-group-management/src/components/CommodityList/Eusm/tests/list.test.tsx create mode 100644 packages/fhir-group-management/src/components/CommodityList/Eusm/tests/viewDetails.test.tsx diff --git a/packages/fhir-group-management/src/components/CommodityList/Eusm/List.tsx b/packages/fhir-group-management/src/components/CommodityList/Eusm/List.tsx new file mode 100644 index 000000000..d41701293 --- /dev/null +++ b/packages/fhir-group-management/src/components/CommodityList/Eusm/List.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { Button, Divider, Dropdown, MenuProps } from 'antd'; +import { MoreOutlined } from '@ant-design/icons'; +import { ADD_EDIT_COMMODITY_URL } 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 { + useSearchParams, + viewDetailsQuery, +} from '@opensrp/react-utils'; +import { + supplyMgSnomedCode, + snomedCodeSystem, +} from '../../../helpers/utils'; +import { useUserRole } from '@opensrp/rbac'; +import { ViewDetailsWrapper } from './ViewDetails'; + +interface GroupListProps { + fhirBaseURL: string; + listId: string; // commodities are added to list resource with this id +} + +/** + * Shows the list of all group and there details + * + * @param props - GroupList component props + * @returns returns healthcare display + */ +export const EusmCommodityList = (props: GroupListProps) => { + const { fhirBaseURL, listId } = props; + + const { t } = useTranslation(); + const { addParam } = useSearchParams(); + const userRole = useUserRole(); + + const getItems = (record: TableData): MenuProps['items'] => { + return [ + { + key: '1', + permissions: [], + label: ( + + ), + }, + ] + .filter((item) => userRole.hasPermissions(item.permissions)) + .map((item) => { + const { permissions, ...rest } = item; + return rest; + }); + }; + + const getColumns = (t: TFunction) => [ + { + title: t("Material Number"), + dataIndex: "materialNumber" as const, + key: "materialNumber" as const, + }, + { + title: t('Name'), + dataIndex: 'name' as const, + key: 'name' as const, + }, + { + title: t("Attractive Item"), + dataIndex: "attractiveItem" as const, + key: "attractiveItem" as const, + render: (value: boolean) =>
{value ? t('Yes') : t('No')}
, + }, + { + title: t('type'), + dataIndex: 'type' as const, + key: 'type' as const, + }, { + title: t('Active'), + dataIndex: 'active' as const, + key: 'active' as const, + render: (value: boolean) =>
{value ? t('Active') : t('Disabled')}
, + }, + { + title: t('Actions'), + width: '10%', + // eslint-disable-next-line react/display-name + render: (_: unknown, record: TableData) => ( + + + {t('Edit')} + + + + + + + ), + }, + ]; + + const baseListViewProps: BaseListViewProps = { + getColumns: getColumns, + createButtonLabel: t('Add Commodity'), + createButtonUrl: ADD_EDIT_COMMODITY_URL, + fhirBaseURL, + pageTitle: t('Commodity List'), + extraQueryFilters: { + code: `${snomedCodeSystem}|${supplyMgSnomedCode}`, + '_has:List:item:_id': listId, + }, + viewDetailsRender: (fhirBaseURL, resourceId) => + }; + + return ; +}; diff --git a/packages/fhir-group-management/src/components/CommodityList/Eusm/ViewDetails.tsx b/packages/fhir-group-management/src/components/CommodityList/Eusm/ViewDetails.tsx new file mode 100644 index 000000000..8af8849d6 --- /dev/null +++ b/packages/fhir-group-management/src/components/CommodityList/Eusm/ViewDetails.tsx @@ -0,0 +1,178 @@ +import React from 'react'; +import { CloseOutlined } from '@ant-design/icons'; +import { Button, Col, Spin, Alert, Space, Image } from 'antd'; +import { Group } from '../../../types'; +import { + getObjLike, + IdentifierUseCodes, + SingleKeyNestedValue, + useSearchParams, + viewDetailsQuery, +} from '@opensrp/react-utils'; +import { get } from 'lodash'; +import { IGroup } from '@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IGroup'; +import { useTranslation } from '../../../mls'; +import { Identifier } from '@smile-cdr/fhirts/dist/FHIR-R4/classes/identifier'; +import { accountabilityCharacteristicCoding, appropriateUsageCharacteristicCoding, attractiveCharacteristicCoding, availabilityCharacteristicCoding, conditionCharacteristicCoding, getCharacteristicWithCoding, useGetGroupAndBinary } from '../../../helpers/utils'; +import { IBinary } from '@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IBinary'; + +/** typings for the view details component */ +export interface EusmViewDetailsProps { + resourceId: string; + fhirBaseURL: string; +} + +export type ViewDetailsWrapperProps = Pick< + EusmViewDetailsProps, + 'fhirBaseURL' +> & { + resourceId?: string; +}; + +/** + * Displays Organization Details + * + * @param props - detail view component props + */ +export const EusmViewDetails = (props: EusmViewDetailsProps) => { + const { resourceId, fhirBaseURL, } = props; + const { t } = useTranslation(); + + const { groupQuery: { data, isLoading, error }, binaryQuery } = useGetGroupAndBinary(resourceId, fhirBaseURL) + + if (isLoading) { + return ; + } + + if (error && !data) { + return ; + } + + const group = data as Group; + const { accountabilityPeriod, + appropriateUsage, + condition, + availability, + attractive, + photoDataUrl, + name, + active, + id, + } = parseEusmCommodity(group, binaryQuery.data); + + const keyValues = { + [t('Product Id')]: id, + [t('Name')]: name, + [t('Active')]: active ? t('Active') : t('Disabled'), + [t('Attractive item')]: attractive, + [t('Is it there')]: availability, + [t('Is it in good condition')]: condition, + [t('Is it being used appropriately')]: appropriateUsage, + [t('accountability period(in months)')]: accountabilityPeriod, + + }; + + return +
+ {photoDataUrl ? product photo : fallbackImage} +
+ {Object.entries(keyValues).map(([key, value]) => { + const props = { + [key]: value, + }; + return value ? ( +
+ +
+ ) : null; + })} +
; +}; + +/** + * component that renders the details view to the right side + * of list view + * + * @param props - detail view component props + */ +export const ViewDetailsWrapper = (props: ViewDetailsWrapperProps) => { + const { resourceId, fhirBaseURL } = props; + const { removeParam } = useSearchParams(); + + if (!resourceId) { + return null; + } + + return ( + +
+
+ + + ); +}; + + +const fallbackImage = + + +/** + * parse a Group to object we can easily consume in Table layout + * + * @param obj - the organization resource object + */ +export const parseEusmCommodity = (obj: IGroup, binary?: IBinary) => { + const { + name, + active, + id, + type, + identifier: rawIdentifier, + } = obj; + const identifierObj = getObjLike( + rawIdentifier, + 'use', + IdentifierUseCodes.OFFICIAL + ) as Identifier[]; + const identifier = get(identifierObj, '0.value'); + const characteristic = obj.characteristic ?? [] + const accountabilityPeriod = getCharacteristicWithCoding(characteristic, accountabilityCharacteristicCoding)?.valueQuantity?.value + const appropriateUsage = getCharacteristicWithCoding(characteristic, appropriateUsageCharacteristicCoding)?.valueCodeableConcept?.text + const condition = getCharacteristicWithCoding(characteristic, conditionCharacteristicCoding)?.valueCodeableConcept?.text + const availability = getCharacteristicWithCoding(characteristic, availabilityCharacteristicCoding)?.valueCodeableConcept?.text + const attractive = getCharacteristicWithCoding(characteristic, attractiveCharacteristicCoding)?.valueBoolean + console.log({binary}) + const photoDataUrl = binary ? `data:${binary.contentType};base64,${binary.data}` : undefined + + return { + accountabilityPeriod, + appropriateUsage, + condition, + availability, + attractive, + photoDataUrl, + name, + active, + id, + identifier, + lastUpdated: get(obj, 'meta.lastUpdated'), + type, + obj, + }; +}; diff --git a/packages/fhir-group-management/src/components/CommodityList/Eusm/tests/__snapshots__/list.test.tsx.snap b/packages/fhir-group-management/src/components/CommodityList/Eusm/tests/__snapshots__/list.test.tsx.snap new file mode 100644 index 000000000..2260bd808 --- /dev/null +++ b/packages/fhir-group-management/src/components/CommodityList/Eusm/tests/__snapshots__/list.test.tsx.snap @@ -0,0 +1,268 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly when listing resources 3`] = ` +
+
+ + Product Id + +
+
+ + 52cffa51-fa81-49aa-9944-5b45d9e4c117 + +
+
+`; + +exports[`renders correctly when listing resources 4`] = ` +
+
+ + Name + +
+
+ + Bed nets + +
+
+`; + +exports[`renders correctly when listing resources 5`] = ` +
+
+ + Active + +
+
+ + Active + +
+
+`; + +exports[`renders correctly when listing resources 6`] = ` +
+
+ + Is it there + +
+
+ + yes + +
+
+`; + +exports[`renders correctly when listing resources 7`] = ` +
+
+ + Is it in good condition + +
+
+ + Yes, no tears, and inocuated + +
+
+`; + +exports[`renders correctly when listing resources 8`] = ` +
+
+ + Is it being used appropriately + +
+
+ + Hanged at correct height and covers averagely sized beds + +
+
+`; + +exports[`renders correctly when listing resources 9`] = ` +
+
+ + accountability period(in months) + +
+
+ + 12 + +
+
+`; + +exports[`renders correctly when listing resources: Header title 1`] = ` + +`; + +exports[`renders correctly when listing resources: table row 1 page 1 1`] = ` + +`; + +exports[`renders correctly when listing resources: table row 1 page 1 2`] = ` + + Bed nets + +`; + +exports[`renders correctly when listing resources: table row 1 page 1 3`] = ` + +
+ No +
+ +`; + +exports[`renders correctly when listing resources: table row 1 page 1 4`] = ` + + substance + +`; + +exports[`renders correctly when listing resources: table row 1 page 1 5`] = ` + +
+ Active +
+ +`; + +exports[`renders correctly when listing resources: table row 1 page 1 6`] = ` + + + + Edit + + - {viewDetailsRender?.(fhirBaseURL, resourceId) ?? } + {viewDetailsRender?.(fhirBaseURL, resourceId) ?? ( + + )} ); diff --git a/packages/fhir-group-management/src/components/BaseComponents/GroupDetail/index.tsx b/packages/fhir-group-management/src/components/BaseComponents/GroupDetail/index.tsx index a80036335..45c598ab5 100644 --- a/packages/fhir-group-management/src/components/BaseComponents/GroupDetail/index.tsx +++ b/packages/fhir-group-management/src/components/BaseComponents/GroupDetail/index.tsx @@ -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< @@ -89,7 +89,7 @@ export const ViewDetails = (props: ViewDetailsProps) => { } const org = data as Group; - return keyValueMapperRenderProp(org, t); + return keyValueMapperRenderProp?.(org, t) ?? null; }; /** From a51569b74e6fc0e5b14f9b4e986b21af5f11807b Mon Sep 17 00:00:00 2001 From: Peter Muriuki Date: Mon, 12 Feb 2024 09:52:29 +0300 Subject: [PATCH 09/32] Conditionally render list view wrt to project code --- .../src/components/CommodityList/index.tsx | 219 +-- .../tests/__snapshots__/index.test.tsx.snap | 1404 ----------------- .../CommodityList/tests/fixtures.ts | 538 ------- .../CommodityList/tests/index.test.tsx | 288 ---- 4 files changed, 8 insertions(+), 2441 deletions(-) delete mode 100644 packages/fhir-group-management/src/components/CommodityList/tests/__snapshots__/index.test.tsx.snap delete mode 100644 packages/fhir-group-management/src/components/CommodityList/tests/fixtures.ts delete mode 100644 packages/fhir-group-management/src/components/CommodityList/tests/index.test.tsx diff --git a/packages/fhir-group-management/src/components/CommodityList/index.tsx b/packages/fhir-group-management/src/components/CommodityList/index.tsx index 717bdc494..251e8a526 100644 --- a/packages/fhir-group-management/src/components/CommodityList/index.tsx +++ b/packages/fhir-group-management/src/components/CommodityList/index.tsx @@ -1,217 +1,14 @@ +import { getConfig } from '@opensrp/pkg-config'; +import { EusmCommodityList } from './Eusm/List'; +import { DefaultCommodityList, GroupListProps } from './Default/List'; 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 { useUserRole } from '@opensrp/rbac'; -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 ( - - {Object.entries(keyValues).map(([key, value]) => { - const props = { - [key]: value, - }; - return value ? ( -
- -
- ) : null; - })} -
- ); -}; - -/** - * Shows the list of all group and there details - * - * @param props - GroupList component props - * @returns returns healthcare display - */ export const CommodityList = (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: ( - - ), - }, - { - key: '2', - permissions: ['Group.delete'], - label: ( - { - 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')); - }); - }} - > - - - ), - }, - ] - .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) =>
{value ? t('Active') : t('Disabled')}
, - }, - { - 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) => ( - - - {t('Edit')} - - - - - - - ), - }, - ]; - - 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 ; -}; + const projectCode = getConfig('projectCode'); -/** - * 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'); + if (projectCode === 'eusm') { + return ; + } else { + return ; } - const disabledGroup: IGroup = { - ...obj, - active: false, - }; - const serve = new FHIRServiceClass(fhirBaseURL, groupResourceType); - const listServer = new FHIRServiceClass(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); - }); }; diff --git a/packages/fhir-group-management/src/components/CommodityList/tests/__snapshots__/index.test.tsx.snap b/packages/fhir-group-management/src/components/CommodityList/tests/__snapshots__/index.test.tsx.snap deleted file mode 100644 index ae5ee5dcd..000000000 --- a/packages/fhir-group-management/src/components/CommodityList/tests/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,1404 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders correctly when listing resources 3`] = ` -
-
- - Commodity Id - -
-
- - e50eb835-7827-4001-b233-e1dda721d4e8 - -
-
-`; - -exports[`renders correctly when listing resources 4`] = ` -
-
- - Name - -
-
- - Amoxicillin 250mg Tablets - -
-
-`; - -exports[`renders correctly when listing resources 5`] = ` -
-
- - Active - -
-
- - Disabled - -
-
-`; - -exports[`renders correctly when listing resources: Header title 1`] = ` - -`; - -exports[`renders correctly when listing resources: table row 1 page 1 1`] = ` - - Albendazole 400mg Tablets - -`; - -exports[`renders correctly when listing resources: table row 1 page 1 2`] = ` - -
- Disabled -
- -`; - -exports[`renders correctly when listing resources: table row 1 page 1 3`] = ` - -`; - -exports[`renders correctly when listing resources: table row 1 page 1 4`] = ` - - - - Edit - -