Skip to content

Commit f6a20a8

Browse files
committed
First iteration of the service point profile view
1 parent f96f504 commit f6a20a8

File tree

19 files changed

+2401
-20
lines changed

19 files changed

+2401
-20
lines changed

app/src/App/fhir-apps.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ import {
8181
import {
8282
LocationUnitList as FHIRLocationUnitList,
8383
NewEditLocationUnit as FHIRNewEditLocationUnit,
84+
URL_LOCATION_VIEW_DETAILS,
85+
ViewDetailsV2
8486
} from '@opensrp/fhir-location-management';
8587
import {
8688
teamAffiliationProps,
@@ -380,6 +382,14 @@ const FHIRApps = () => {
380382
permissions={['Location.update']}
381383
component={FHIRNewEditLocationUnit}
382384
/>
385+
<PrivateComponent
386+
redirectPath={APP_CALLBACK_URL}
387+
disableLoginProtection={DISABLE_LOGIN_PROTECTION}
388+
exact
389+
path={`${URL_LOCATION_VIEW_DETAILS}/:id`}
390+
permissions={['Location.read']}
391+
component={ViewDetailsV2}
392+
/>
383393
<PrivateComponent
384394
redirectPath={APP_CALLBACK_URL}
385395
disableLoginProtection={DISABLE_LOGIN_PROTECTION}

packages/fhir-care-team/src/components/ListView/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React from 'react';
33
import { Helmet } from 'react-helmet';
44
import { Row, Col, Button, Divider, Dropdown, Popconfirm } from 'antd';
55
import type { MenuProps } from 'antd';
6-
import { PageHeader } from '@opensrp/react-utils';
6+
import { PageHeader, useTabularViewWithLocalSearch } from '@opensrp/react-utils';
77
import { MoreOutlined, PlusOutlined } from '@ant-design/icons';
88
import { RouteComponentProps } from 'react-router';
99
import { useHistory, Link } from 'react-router-dom';

packages/fhir-location-management/src/components/LocationUnitList/Table.tsx

+3-9
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Button, Divider, Dropdown } from 'antd';
33
import type { MenuProps } from 'antd';
44
import { MoreOutlined } from '@ant-design/icons';
55
import { Link } from 'react-router-dom';
6-
import { URL_LOCATION_UNIT_EDIT } from '../../constants';
6+
import { URL_LOCATION_UNIT_EDIT, URL_LOCATION_VIEW_DETAILS } from '../../constants';
77
import { Column, TableLayout } from '@opensrp/react-utils';
88
import { useTranslation } from '../../mls';
99
import { RbacCheck } from '@opensrp/rbac';
@@ -49,15 +49,9 @@ const Table: React.FC<Props> = (props: Props) => {
4949
{
5050
key: '1',
5151
label: (
52-
<Button
53-
type="link"
54-
data-testid="view-location"
55-
onClick={() => {
56-
onViewDetails?.(record);
57-
}}
58-
>
52+
<Link to={`${URL_LOCATION_VIEW_DETAILS}/${record.id}`} className="m-0 p-1">
5953
{t('View details')}
60-
</Button>
54+
</Link>
6155
),
6256
},
6357
];

packages/fhir-location-management/src/components/LocationUnitList/index.tsx

-7
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,6 @@ export const LocationUnitList: React.FC<LocationUnitListProps> = (props: Locatio
180180
/>
181181
</div>
182182
</Col>
183-
{detailId ? (
184-
<LocationUnitDetail
185-
fhirBaseUrl={fhirBaseURL}
186-
onClose={() => setDetailId('')}
187-
detailId={detailId}
188-
/>
189-
) : null}
190183
</Row>
191184
</section>
192185
</>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
.table-container {
2+
background-color: #ffffff;
3+
margin: 20px;
4+
}
5+
6+
.location-table-action {
7+
width: 86px;
8+
}
9+
10+
.location-table-action .edit {
11+
width: 45px;
12+
color: #1890ff;
13+
}
14+
15+
.location-table-action .more-options {
16+
cursor: pointer;
17+
width: 32px;
18+
color: #1890ff;
19+
border-left: 1px solid;
20+
height: 16px;
21+
font-size: 18px;
22+
border-left-color: #e9e9e9;
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import React from 'react';
2+
import { Button, Divider, Dropdown } from 'antd';
3+
import type { MenuProps } from 'antd';
4+
import { MoreOutlined } from '@ant-design/icons';
5+
import { Link } from 'react-router-dom';
6+
import { URL_LOCATION_UNIT_EDIT } from '../../constants';
7+
import { Column, TableLayout } from '@opensrp/react-utils';
8+
import { useTranslation } from '../../mls';
9+
import { RbacCheck } from '@opensrp/rbac';
10+
11+
export interface TableData {
12+
id: string;
13+
key: string;
14+
name: string;
15+
description?: string;
16+
status?: string;
17+
physicalType?: string;
18+
partOf?: string;
19+
}
20+
21+
export interface Props {
22+
data: TableData[];
23+
onViewDetails?: (row: TableData) => void;
24+
}
25+
26+
const Table: React.FC<Props> = (props: Props) => {
27+
const { onViewDetails } = props;
28+
const { t } = useTranslation();
29+
const columns: Column<TableData>[] = [
30+
{
31+
title: t('Name'),
32+
dataIndex: 'name',
33+
},
34+
{
35+
title: t('Parent'),
36+
dataIndex: 'partOf',
37+
},
38+
{
39+
title: t('Physical Type'),
40+
dataIndex: 'physicalType',
41+
},
42+
{
43+
title: t('Status'),
44+
dataIndex: 'status',
45+
},
46+
];
47+
48+
const getItems = (record: TableData): MenuProps['items'] => [
49+
{
50+
key: '1',
51+
label: (
52+
<Button
53+
type="link"
54+
data-testid="view-location"
55+
onClick={() => {
56+
onViewDetails?.(record);
57+
}}
58+
>
59+
{t('View details')}
60+
</Button>
61+
),
62+
},
63+
];
64+
65+
return (
66+
<TableLayout
67+
id="LocationUnitList"
68+
persistState={true}
69+
datasource={props.data}
70+
columns={columns}
71+
actions={{
72+
title: t('Actions'),
73+
width: '10%',
74+
// eslint-disable-next-line react/display-name
75+
render: (_: boolean, record) => (
76+
<>
77+
<RbacCheck permissions={['Location.update']}>
78+
<>
79+
<Link to={`${URL_LOCATION_UNIT_EDIT}/${record.id}`} className="m-0 p-1">
80+
{t('Edit')}
81+
</Link>
82+
<Divider type="vertical" />
83+
</>
84+
</RbacCheck>
85+
<Dropdown
86+
menu={{ items: getItems(record) }}
87+
placement="bottomRight"
88+
arrow
89+
trigger={['click']}
90+
>
91+
<MoreOutlined className="more-options" />
92+
</Dropdown>
93+
</>
94+
),
95+
}}
96+
/>
97+
);
98+
};
99+
100+
export default Table;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import React, { useState } from 'react';
2+
import { Helmet } from 'react-helmet';
3+
import { get } from 'lodash';
4+
import { Row, Col, Button, Spin, Alert } from 'antd';
5+
import { PageHeader } from '@opensrp/react-utils';
6+
import { PlusOutlined } from '@ant-design/icons';
7+
import { LocationUnitDetail } from '../LocationUnitDetail';
8+
import { useHistory } from 'react-router-dom';
9+
import { BrokenPage, Resource404 } from '@opensrp/react-utils';
10+
import { URL_LOCATION_UNIT_ADD } from '../../constants';
11+
import Table, { TableData } from './Table';
12+
import Tree from '../LocationTree';
13+
import { useGetLocationHierarchy } from '../../helpers/utils';
14+
import './LocationUnitList.css';
15+
import { TreeNode } from '../../helpers/types';
16+
import { useSelector, useDispatch } from 'react-redux';
17+
import reducerRegistry from '@onaio/redux-reducer-registry';
18+
import {
19+
reducerName,
20+
reducer,
21+
setSelectedNode,
22+
getSelectedNode,
23+
} from '../../ducks/location-tree-state';
24+
import { useTranslation } from '../../mls';
25+
import { RbacCheck } from '@opensrp/rbac';
26+
import { RootLocationWizard } from '../RootLocationWizard';
27+
28+
reducerRegistry.register(reducerName, reducer);
29+
30+
interface LocationUnitListProps {
31+
fhirBaseURL: string;
32+
fhirRootLocationId: string; // This is the location.id field.
33+
}
34+
35+
export interface AntTreeData {
36+
data: TreeNode;
37+
title: JSX.Element;
38+
key: string;
39+
children: AntTreeData[];
40+
}
41+
42+
/**
43+
* Parse the hierarchy node into table data
44+
*
45+
* @param hierarchy - hierarchy node to be parsed
46+
* @returns array of table data
47+
*/
48+
export function parseTableData(hierarchy: TreeNode[]) {
49+
const data: TableData[] = [];
50+
hierarchy.forEach((location) => {
51+
const { model } = location;
52+
data.push({
53+
id: model.node.id,
54+
key: model.nodeId,
55+
name: model.node.name,
56+
partOf: model.node.partOf?.display ?? '-',
57+
description: model.node?.description,
58+
status: model.node?.status,
59+
physicalType: get(model.node, 'physicalType.coding.0.display'),
60+
});
61+
});
62+
return data;
63+
}
64+
65+
export const LocationUnitList: React.FC<LocationUnitListProps> = (props: LocationUnitListProps) => {
66+
const { fhirBaseURL, fhirRootLocationId } = props;
67+
const [detailId, setDetailId] = useState<string>();
68+
const selectedNode = useSelector((state) => getSelectedNode(state));
69+
const dispatch = useDispatch();
70+
const { t } = useTranslation();
71+
const history = useHistory();
72+
const [showWizard, setShowWizard] = useState(false);
73+
74+
// get parent location Id
75+
76+
// get the root locations. the root node is the opensrp root location, its immediate children
77+
// are the user-defined root locations.
78+
const {
79+
data: treeData,
80+
isLoading: treeIsLoading,
81+
error: treeError,
82+
isFetching: treeIsFetching,
83+
} = useGetLocationHierarchy(fhirBaseURL, fhirRootLocationId, {
84+
enabled: !showWizard,
85+
onError: (error) => {
86+
if (error.statusCode === 404) {
87+
setShowWizard(true);
88+
}
89+
},
90+
});
91+
92+
if (treeIsLoading) {
93+
return <Spin size="large" className="custom-spinner" />;
94+
}
95+
96+
if (showWizard) {
97+
return <RootLocationWizard fhirBaseUrl={fhirBaseURL} rootLocationId={fhirRootLocationId} />;
98+
}
99+
100+
if (treeError && !treeData) {
101+
return <BrokenPage errorMessage={`${treeError.message}`} />;
102+
}
103+
104+
if (!treeData) {
105+
return <Resource404 />;
106+
}
107+
108+
// generate table data; consider if there is a selected node, sorting the data
109+
const toDispNodes =
110+
(selectedNode ? (selectedNode.children as TreeNode[]) : treeData.children) ?? [];
111+
const sortedNodes = [...toDispNodes].sort((a, b) =>
112+
a.model.node.name.localeCompare(b.model.node.name)
113+
);
114+
let tableNodes = sortedNodes;
115+
// if a node is selected only its children should be selected, the selected node should come first anyway.
116+
if (selectedNode) {
117+
tableNodes = [selectedNode, ...sortedNodes];
118+
}
119+
const tableDispData = parseTableData(tableNodes);
120+
const pageTitle = t('Location Unit Management');
121+
122+
return (
123+
<>
124+
{treeIsFetching && (
125+
<Alert
126+
type="info"
127+
message={t('Refreshing data')}
128+
description={t(
129+
'Request to update the location hierarchy is taking a bit long to respond.'
130+
)}
131+
banner
132+
showIcon
133+
/>
134+
)}
135+
<section className="content-section">
136+
<Helmet>
137+
<title>{pageTitle}</title>
138+
</Helmet>
139+
<PageHeader title={pageTitle} />
140+
<Row>
141+
<Col className="bg-white p-3" span={6}>
142+
<Tree
143+
data-testid="hierarchy-display"
144+
data={treeData.children}
145+
selectedNode={selectedNode}
146+
onSelect={(node) => {
147+
dispatch(setSelectedNode(node));
148+
}}
149+
/>
150+
</Col>
151+
<Col className="bg-white p-3 border-left" span={detailId ? 13 : 18}>
152+
<div className="mb-3 d-flex justify-content-between p-3">
153+
<h6 className="mt-4">
154+
{selectedNode ? selectedNode.model.node.name : t('Location Unit')}
155+
</h6>
156+
<RbacCheck permissions={['Location.create']}>
157+
<div>
158+
<Button
159+
type="primary"
160+
onClick={() => {
161+
if (selectedNode) {
162+
const queryParams = { parentId: selectedNode.model.nodeId };
163+
const searchString = new URLSearchParams(queryParams).toString();
164+
history.push(`${URL_LOCATION_UNIT_ADD}?${searchString}`);
165+
} else {
166+
history.push(URL_LOCATION_UNIT_ADD);
167+
}
168+
}}
169+
>
170+
<PlusOutlined />
171+
{t('Add Location Unit')}
172+
</Button>
173+
</div>
174+
</RbacCheck>
175+
</div>
176+
<div className="bg-white p-3">
177+
<Table
178+
data={tableDispData}
179+
onViewDetails={async (row) => {
180+
setDetailId(row.id);
181+
}}
182+
/>
183+
</div>
184+
</Col>
185+
</Row>
186+
</section>
187+
</>
188+
);
189+
};

0 commit comments

Comments
 (0)