diff --git a/Dockerfile b/Dockerfile index 19d742ae0..e80f826c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,11 @@ FROM alpine/git AS sources -RUN git clone --depth=1 --branch=v2.0.1 https://github.com/onaio/express-server.git /usr/src/express-server +# TODO - update the tag here +RUN git clone --branch=bulk-upload-bull https://github.com/onaio/express-server.git /usr/src/express-server + +WORKDIR /usr/src/express-server + +RUN git checkout 378f2884 FROM node:16.18-alpine as build @@ -21,7 +26,6 @@ RUN chown -R node . USER node RUN yarn lerna run build - FROM node:16.18-alpine as nodejsbuild COPY --from=sources /usr/src/express-server /usr/src/express-server @@ -32,7 +36,9 @@ RUN yarn && yarn tsc && npm prune -production --legacy-peer-deps # Remove unused dependencies RUN rm -rf ./node_modules/typescript -FROM node:16.18-alpine as final +# TODO - change image to use one with python or install python here +FROM nikolaik/python-nodejs:python3.12-nodejs22-alpine as final + # Use tini for NodeJS application https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#handling-kernel-signals RUN apk add --no-cache tini curl @@ -52,21 +58,28 @@ WORKDIR /usr/src/web COPY --from=build /project/node_modules /usr/src/web/node_modules COPY --from=build /project/app/build /usr/src/web -RUN chown -R node /usr/src/web + +# RUN chown -R pn /usr/src/web WORKDIR /usr/src/app -COPY --from=nodejsbuild /usr/src/express-server/dist /usr/src/app +COPY --from=nodejsbuild /usr/src/express-server/build /usr/src/app COPY --from=nodejsbuild /usr/src/express-server/node_modules /usr/src/app/node_modules -RUN chown -R node /usr/src/app -USER node +RUN pip install -r /usr/src/app/fhir-tooling/requirements.txt + +# RUN chown -R pn /usr/src/app + +# USER pn ENV EXPRESS_REACT_BUILD_PATH /usr/src/web/ EXPOSE 3000 -CMD [ "/bin/sh", "-c", "/usr/local/bin/app.sh && node ." ] +CMD [ "/bin/sh", "-c", "/usr/local/bin/app.sh && node /usr/src/app/dist" ] + +# ENTRYPOINT [ "/bin/sh" ] +# ENTRYPOINT [ "/bin/sh", "-c", "/usr/local/bin/app.sh && node ." ] ENTRYPOINT ["/sbin/tini", "--"] diff --git a/app/src/App/fhir-apps.tsx b/app/src/App/fhir-apps.tsx index d7a8c1a60..3808acdb4 100644 --- a/app/src/App/fhir-apps.tsx +++ b/app/src/App/fhir-apps.tsx @@ -126,6 +126,7 @@ import { CloseFlag, URL_CLOSE_FLAGS } from '@opensrp/fhir-flag'; import { useTranslation } from '../mls'; import '@opensrp/user-management/dist/index.css'; import { APP_LOGIN_URL } from '../configs/dispatchConfig'; +import { DATA_IMPORT_CREATE_URL, ImportDetailViewDetails, DATA_IMPORT_DETAIL_URL, DATA_IMPORT_LIST_URL, DataImportList, StartDataImport } from '@opensrp/fhir-import'; /** Util function that renders Oauth2 callback components * @@ -203,6 +204,40 @@ const FHIRApps = () => { permissions={['iam_group.read']} component={UserGroupsList} /> + + + + }, + title: t('Data Imports'), + key: 'data-import', + enabled: true, + permissions: ['WebDataImport.read'], + url: DATA_IMPORT_LIST_URL, + isHomePageLink: true, + }, ]; return filterFalsyRoutes(routes, userRole); diff --git a/packages/fhir-import/README.md b/packages/fhir-import/README.md new file mode 100644 index 000000000..d5d275b50 --- /dev/null +++ b/packages/fhir-import/README.md @@ -0,0 +1,46 @@ +# Package + + + +## Installation + +```sh +yarn add @opensrp/template +``` + + + +## Usage + + + +### Code examples + + diff --git a/packages/fhir-import/index.d.ts b/packages/fhir-import/index.d.ts new file mode 100644 index 000000000..1fbea2e91 --- /dev/null +++ b/packages/fhir-import/index.d.ts @@ -0,0 +1,144 @@ +declare module '@2fd/ant-design-icons' { + import { ReactNode, SVGProps } from 'react'; + + export interface IconProps extends SVGProps { + title?: string; + size?: number | string; + color?: string; + } + + export interface IconComponent { + (props: IconProps): ReactNode; + } + + export const AccountBookOutlined: IconComponent; + export const AlertOutlined: IconComponent; + export const AliyunOutlined: IconComponent; + export const AmazonOutlined: IconComponent; + export const AndroidOutlined: IconComponent; + export const AppleOutlined: IconComponent; + export const AppstoreOutlined: IconComponent; + export const ArrowDownOutlined: IconComponent; + export const ArrowLeftOutlined: IconComponent; + export const ArrowRightOutlined: IconComponent; + export const ArrowUpOutlined: IconComponent; + export const AudioOutlined: IconComponent; + export const BackwardOutlined: IconComponent; + export const BankOutlined: IconComponent; + export const BarcodeOutlined: IconComponent; + export const BarsOutlined: IconComponent; + export const BellOutlined: IconComponent; + export const BookOutlined: IconComponent; + export const BoxPlotOutlined: IconComponent; + export const BugOutlined: IconComponent; + export const BuildOutlined: IconComponent; + export const BulbOutlined: IconComponent; + export const CalculatorOutlined: IconComponent; + export const CalendarOutlined: IconComponent; + export const CameraOutlined: IconComponent; + export const CarOutlined: IconComponent; + export const CarryOutOutlined: IconComponent; + export const CheckCircleOutlined: IconComponent; + export const CheckSquareOutlined: IconComponent; + export const ClockCircleOutlined: IconComponent; + export const CloudOutlined: IconComponent; + export const CodeOutlined: IconComponent; + export const CoffeeOutlined: IconComponent; + export const CompassOutlined: IconComponent; + export const ContactsOutlined: IconComponent; + export const ContainerOutlined: IconComponent; + export const ControlOutlined: IconComponent; + export const CreditCardOutlined: IconComponent; + export const CrownOutlined: IconComponent; + export const CustomerServiceOutlined: IconComponent; + export const DashboardOutlined: IconComponent; + export const DatabaseOutlined: IconComponent; + export const DeleteOutlined: IconComponent; + export const DesktopOutlined: IconComponent; + export const DislikeOutlined: IconComponent; + export const DollarOutlined: IconComponent; + export const DownloadOutlined: IconComponent; + export const EditOutlined: IconComponent; + export const EnvironmentOutlined: IconComponent; + export const ExclamationCircleOutlined: IconComponent; + export const ExperimentOutlined: IconComponent; + export const ExportOutlined: IconComponent; + export const EyeOutlined: IconComponent; + export const FileOutlined: IconComponent; + export const FilterOutlined: IconComponent; + export const FireOutlined: IconComponent; + export const FlagOutlined: IconComponent; + export const FolderOutlined: IconComponent; + export const FolderOpenOutlined: IconComponent; + export const FundProjectionScreenOutlined: IconComponent; + export const FunnelPlotOutlined: IconComponent; + export const GiftOutlined: IconComponent; + export const GlobalOutlined: IconComponent; + export const GoldOutlined: IconComponent; + export const HddOutlined: IconComponent; + export const HeartOutlined: IconComponent; + export const HomeOutlined: IconComponent; + export const HourglassOutlined: IconComponent; + export const IdcardOutlined: IconComponent; + export const InboxOutlined: IconComponent; + export const KeyOutlined: IconComponent; + export const LaptopOutlined: IconComponent; + export const LayoutOutlined: IconComponent; + export const LikeOutlined: IconComponent; + export const LineChartOutlined: IconComponent; + export const LockOutlined: IconComponent; + export const MailOutlined: IconComponent; + export const MedicineBoxOutlined: IconComponent; + export const MehOutlined: IconComponent; + export const MessageOutlined: IconComponent; + export const MobileOutlined: IconComponent; + export const MoneyCollectOutlined: IconComponent; + export const NotificationOutlined: IconComponent; + export const OrderedListOutlined: IconComponent; + export const PaperClipOutlined: IconComponent; + export const PartitionOutlined: IconComponent; + export const PauseOutlined: IconComponent; + export const PhoneOutlined: IconComponent; + export const PictureOutlined: IconComponent; + export const PlayCircleOutlined: IconComponent; + export const PlusOutlined: IconComponent; + export const PrinterOutlined: IconComponent; + export const ProfileOutlined: IconComponent; + export const ProjectOutlined: IconComponent; + export const PushpinOutlined: IconComponent; + export const QrcodeOutlined: IconComponent; + export const ReadOutlined: IconComponent; + export const ReconciliationOutlined: IconComponent; + export const RedEnvelopeOutlined: IconComponent; + export const ReloadOutlined: IconComponent; + export const RestOutlined: IconComponent; + export const RocketOutlined: IconComponent; + export const SafetyCertificateOutlined: IconComponent; + export const SaveOutlined: IconComponent; + export const ScheduleOutlined: IconComponent; + export const SecurityScanOutlined: IconComponent; + export const SettingOutlined: IconComponent; + export const ShopOutlined: IconComponent; + export const ShoppingCartOutlined: IconComponent; + export const SkinOutlined: IconComponent; + export const SmileOutlined: IconComponent; + export const SolutionOutlined: IconComponent; + export const StarOutlined: IconComponent; + export const StopOutlined: IconComponent; + export const SwitcherOutlined: IconComponent; + export const TabletOutlined: IconComponent; + export const TagOutlined: IconComponent; + export const TagsOutlined: IconComponent; + export const ToolOutlined: IconComponent; + export const UnlockOutlined: IconComponent; + export const UsergroupAddOutlined: IconComponent; + export const UsergroupDeleteOutlined: IconComponent; + export const UserOutlined: IconComponent; + export const VideoCameraOutlined: IconComponent; + export const WalletOutlined: IconComponent; + export const WarningOutlined: IconComponent; + export const WifiOutlined: IconComponent; + export const ZoomInOutlined: IconComponent; + export const ZoomOutOutlined: IconComponent; + } + \ No newline at end of file diff --git a/packages/fhir-import/package.json b/packages/fhir-import/package.json new file mode 100644 index 000000000..003709ead --- /dev/null +++ b/packages/fhir-import/package.json @@ -0,0 +1,47 @@ +{ + "name": "@opensrp/fhir-import", + "version": "0.0.6", + "description": "", + "main": "dist/index.js", + "types": "dist/types", + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + }, + "repository": "https://github.com/opensrp/web", + "scripts": { + "test": "run -T test $INIT_CWD --verbose", + "tsc": "run -T tsc", + "lint": "run -T eslint ./**/*.{js,jsx,ts,tsx}", + "copy": "run -T copyfiles -u 1 \"./src/**/*.{css,html}\" \"./dist/\"", + "build": "run tsc && run transpile && run copy", + "transpile": "run -T babel src -d dist --root-mode upward --extensions .ts,.tsx --ignore '**/*.test.ts,**/*.test.tsx,**/tests,**/__tests__'" + }, + "jest": { + "automock": false, + "setupFiles": [ + "../../setupTests" + ] + }, + "bugs": { + "url": "https://github.com/opensrp/web/issues" + }, + "peerDependencies": { + "@opensrp/i18n": "^0.0.1", + "react": "^17.0.0", + "react-dom": "17.0.0", + "react-query": "^3.15.1", + "react-router": "^5.2.0", + "react-router-dom": "^5.2.0" + }, + "dependencies": { + "@2fd/ant-design-icons": "^2.6.0", + "@opensrp/notifications": "^0.0.5", + "@opensrp/rbac": "workspace:^", + "@opensrp/react-utils": "^0.0.12" + }, + "author": "OpenSRP Engineering", + "license": "Apache-2.0" +} diff --git a/packages/fhir-import/src/components/statusTag.tsx b/packages/fhir-import/src/components/statusTag.tsx new file mode 100644 index 000000000..b76375f0e --- /dev/null +++ b/packages/fhir-import/src/components/statusTag.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Tag } from 'antd'; + +export const ImportStatusTag = ({ statusString }: { statusString: JobStatus }) => { + const tagStatusColor = getStatusColor(statusString) + return ( + {statusString} + ) +} + +type JobStatus = + | 'completed' + | 'waiting' + | 'active' + | 'delayed' + | 'failed' + | 'paused'; + +export function getStatusColor(statusString: JobStatus) { + switch (statusString) { + case "completed": + return "success" + case "active": + return "processing" + case "failed": + return "error" + case "paused": + return "warning" + default: + return "default" + } +} \ No newline at end of file diff --git a/packages/fhir-import/src/constants.ts b/packages/fhir-import/src/constants.ts new file mode 100644 index 000000000..2e7ed6d69 --- /dev/null +++ b/packages/fhir-import/src/constants.ts @@ -0,0 +1,19 @@ +export const DATA_IMPORT_LIST_URL = "/import" +export const DATA_IMPORT_DETAIL_URL = "/importDetail" +export const DATA_IMPORT_CREATE_URL = "/importCreate" + +export const IMPORT_ENDPOINT = "/$import" +export const IMPORT_DOMAIN_URI="" +export const IMPORT_TEMPLATE_ENDPOINT = `${IMPORT_ENDPOINT}/templates` + + +export const dataImportRQueryKey = "dataImport" +export const locations = "locations" as const +export const users = "users" as const +export const organizations = "organizations" as const +export const careteams = "careteams" as const +export const inventories = "inventories" as const +export const orgToLocationAssignment = "orgToLocationAssignment" as const +export const userToOrganizationAssignment = "userToOrganizationAssignment" as const +export const products = "products" as const +export const productImages = "productImages" as const diff --git a/packages/fhir-import/src/containers/ImportDetailOverView/index.css b/packages/fhir-import/src/containers/ImportDetailOverView/index.css new file mode 100644 index 000000000..a316a09b2 --- /dev/null +++ b/packages/fhir-import/src/containers/ImportDetailOverView/index.css @@ -0,0 +1,23 @@ +.terminal-output { + color: #cccccc; + font-family: 'Courier New', Courier, monospace; + background-color: #1e1e1e; + border: 1px solid #555555; + border-radius: 5px; + padding: 20px; + margin: 20px auto; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + overflow: auto; + max-height: 600px; +} + +.terminal-output pre { + white-space: pre-wrap; + /* Makes sure that text wraps within the pre element */ + word-wrap: break-word; + word-break: break-all; + /* Ensures long words will wrap and not overflow the container */ + margin: 0; + color: #fff; + /* Color for the terminal text */ +} \ No newline at end of file diff --git a/packages/fhir-import/src/containers/ImportDetailOverView/index.tsx b/packages/fhir-import/src/containers/ImportDetailOverView/index.tsx new file mode 100644 index 000000000..8887c77c7 --- /dev/null +++ b/packages/fhir-import/src/containers/ImportDetailOverView/index.tsx @@ -0,0 +1,136 @@ +import React, { Fragment } from 'react'; +import { Col, Button, Alert, Spin, Tabs } from 'antd'; +import { CloseOutlined, SyncOutlined } from '@ant-design/icons'; +import { useQuery } from 'react-query'; +import { + BrokenPage, + FHIRServiceClass, + getObjLike, + IdentifierUseCodes, + getResourcesFromBundle, + parseFhirHumanName, + viewDetailsQuery, + useSearchParams, +} from '@opensrp/react-utils'; +import { dataImportRQueryKey, IMPORT_DOMAIN_URI } from '../../constants'; +import { useTranslation } from '../../mls'; +import { OpenSRPService, BodyLayout, ResourceDetails, Resource404, KeyValuesDescriptions } from '@opensrp/react-utils'; +import { Helmet } from 'react-helmet'; +import { useParams } from 'react-router'; +import { formatTimestamp } from '../../helpers/utils' +import { getStatusColor } from '../../components/statusTag' +import "./index.css"; + +/** typings for the view details component */ +export interface RouteComponentProps { + workflowId: string; +} + +/** + * component that renders the details view to the right side + * of list view + * + * @param props - detail view component props + */ +export const ImportDetailViewDetails = (props: RouteComponentProps) => { + const params = useParams(); + const workflowId = params.workflowId; + const { t } = useTranslation(); + const { removeParam } = useSearchParams(); + console.log("We got here", { workflowId }) + + const { data, isLoading, error } = useQuery( + [dataImportRQueryKey, workflowId], () => { + const service = new OpenSRPService(`/$import`, IMPORT_DOMAIN_URI) + return service.read(workflowId).then(res => { + return res + }) + }, { + enabled: !!workflowId, + } + ); + + + if (isLoading) { + return ; + } + + if (error && !data) { + return ; + } + + if (!data) { + return ; + } + + const pageTitle = t(`View details | {{workflowId}}`, { workflowId: data.workflowId }); + const headerProps = { + pageHeaderProps: { + title: pageTitle, + onBack: undefined, + }, + }; + + + const dateCreatedKeyPairing = { + [t('Date Created')]: formatTimestamp(data.dateCreated), + }; + + const headerLeftData = { + ID: data.workflowId, + }; + + const otherDetailsMap = { + [t('Workflow type')]: data.workflowType, + [t('Date Started')]: formatTimestamp(data.dateStarted), + [t('Date Ended')]: formatTimestamp(data.dateEnded), + [t('Author')]: data.author + }; + + return ( + + + {pageTitle} + + + } + /> + + + {data.statusReason?.stdout} + {data.statusReason?.stderr} + + , + }]} + /> + + + ); +}; + diff --git a/packages/fhir-import/src/containers/ImportDetailOverView/tests/__snapshots__/index.test.tsx.snap b/packages/fhir-import/src/containers/ImportDetailOverView/tests/__snapshots__/index.test.tsx.snap new file mode 100644 index 000000000..66bb3d084 --- /dev/null +++ b/packages/fhir-import/src/containers/ImportDetailOverView/tests/__snapshots__/index.test.tsx.snap @@ -0,0 +1,408 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`1157 - view details errors out for careTeam 3500 1`] = ` + + + CareTeam ID + + + 3500 + + +`; + +exports[`1157 - view details errors out for careTeam 3500 2`] = ` + + + Name + + + Peter James Charlmers Care team + + +`; + +exports[`1157 - view details errors out for careTeam 3500 3`] = ` + + + status + + + active + + +`; + +exports[`1157 - view details errors out for careTeam 3500 4`] = ` + + + Participants + + + + + + + + + Patient + + + + + Peter James Chalmers + + + + + + + + + + +`; + +exports[`1157 - view details errors out for careTeam 3500 5`] = ` + + + Patient + + + + + Peter James Chalmers + + + + +`; + +exports[`1157 - view details errors out for careTeam 3500 6`] = ` + + + Managing organizations + + + + + + No managing organizations found + + + + + +`; + +exports[`Closes on clicking cancel (X) 1`] = ` + + + CareTeam ID + + + 142534 + + +`; + +exports[`Closes on clicking cancel (X) 2`] = ` + + + Identifier + + + 99c4dde5-3aca-4a4b-8b33-b50142e05da6 + + +`; + +exports[`Closes on clicking cancel (X) 3`] = ` + + + Name + + + Brown Bag + + +`; + +exports[`Closes on clicking cancel (X) 4`] = ` + + + status + + + active + + +`; + +exports[`Closes on clicking cancel (X) 5`] = ` + + + Participants + + + + + + + + + Practitioner + + + + + AeHIN Demo + + + + + + + + + + +`; + +exports[`Closes on clicking cancel (X) 6`] = ` + + + Practitioner + + + + + AeHIN Demo + + + + +`; + +exports[`Closes on clicking cancel (X) 7`] = ` + + + Managing organizations + + + + + + No managing organizations found + + + + + +`; + +exports[`works correctly 1`] = ` + + + CareTeam ID + + + 131411 + + +`; + +exports[`works correctly 2`] = ` + + + Identifier + + + 93bc9c3d-6321-41b0-9b93-1275d7114e22 + + +`; + +exports[`works correctly 3`] = ` + + + Name + + + Care Team One + + +`; + +exports[`works correctly 4`] = ` + + + status + + + active + + +`; + +exports[`works correctly 5`] = ` + + + Participants + + + + + + + + + Practitioner + + + + + Ward test hey N test Tester family + + + Ward test hey N test Tester family + + + + + + + + + + +`; + +exports[`works correctly 6`] = ` + + + Practitioner + + + + + Ward test hey N test Tester family + + + Ward test hey N test Tester family + + + + +`; + +exports[`works correctly 7`] = ` + + + Managing organizations + + + + + + No managing organizations found + + + + + +`; diff --git a/packages/fhir-import/src/containers/ImportDetailOverView/tests/fixtures.ts b/packages/fhir-import/src/containers/ImportDetailOverView/tests/fixtures.ts new file mode 100644 index 000000000..f0ac2811a --- /dev/null +++ b/packages/fhir-import/src/containers/ImportDetailOverView/tests/fixtures.ts @@ -0,0 +1,480 @@ +export const careTeamWithIncluded = { + resourceType: 'Bundle', + id: '2fb071f1-cafc-4a84-9d0d-e7bdb2e7875e', + meta: { + lastUpdated: '2022-09-29T20:32:11.641+00:00', + }, + type: 'searchset', + total: 1, + link: [ + { + relation: 'self', + url: 'https://fhir.labs.smartregister.org:443/fhir/CareTeam/_search?_id=131411&_include=CareTeam%3A*', + }, + ], + entry: [ + { + fullUrl: 'https://fhir.labs.smartregister.org:443/fhir/CareTeam/131411', + resource: { + resourceType: 'CareTeam', + id: '131411', + meta: { + versionId: '1', + lastUpdated: '2022-05-30T00:53:35.099+00:00', + source: '#8pT6h5Axyf9VsdQq', + }, + identifier: [ + { + use: 'official', + value: '93bc9c3d-6321-41b0-9b93-1275d7114e22', + }, + ], + status: 'active', + name: 'Care Team One', + subject: { + reference: 'Group/131410', + }, + participant: [ + { + member: { + reference: 'Practitioner/131406', + }, + }, + { + member: { + reference: 'Practitioner/131406', + }, + }, + ], + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: 'https://fhir.labs.smartregister.org:443/fhir/Group/131410', + resource: { + resourceType: 'Group', + id: '131410', + meta: { + versionId: '2', + lastUpdated: '2022-06-27T03:22:27.188+00:00', + source: '#c6f633c24d9e6c4b', + }, + identifier: [ + { + use: 'official', + value: '93bc9c3d-6321-41b0-9b93-1275d7114e34', + }, + ], + active: true, + name: 'ANC patients', + quantity: 1, + member: [ + { + entity: { + reference: 'Patient/131408', + }, + }, + ], + }, + search: { + mode: 'include', + }, + }, + { + fullUrl: 'https://fhir.labs.smartregister.org:443/fhir/Practitioner/131406', + resource: { + resourceType: 'Practitioner', + id: '131406', + meta: { + versionId: '1', + lastUpdated: '2022-05-30T00:38:44.891+00:00', + source: '#R1qpXIa2QDrDkBrn', + }, + identifier: [ + { + use: 'official', + value: 'aace2e430b-64be-477e-9d86-b36c666c0211', + }, + { + use: 'secondary', + value: '40353ad0-6fa0-4da3-9dd6-b2d9d5a09b6a', + }, + ], + active: true, + name: [ + { + use: 'official', + family: 'Tester family', + given: ['Ward test hey', 'N test'], + }, + ], + telecom: [ + { + system: 'email', + value: 'reham.muzzamil@venturedive.com', + }, + ], + }, + search: { + mode: 'include', + }, + }, + ], +}; + +export const careTeam2 = { + resourceType: 'Bundle', + id: '11112110-5942-40cb-b8ca-80650821dba4', + meta: { + lastUpdated: '2022-09-30T06:13:08.311+00:00', + }, + type: 'searchset', + total: 1, + link: [ + { + relation: 'self', + url: 'https://fhir.labs.smartregister.org:443/fhir/CareTeam?_format=json&_id=142534&_include=CareTeam%3A*', + }, + ], + entry: [ + { + fullUrl: 'https://fhir.labs.smartregister.org:443/fhir/CareTeam/142534', + resource: { + resourceType: 'CareTeam', + id: '142534', + meta: { + versionId: '1', + lastUpdated: '2022-09-01T12:44:38.522+00:00', + source: '#532e59e3409867b3', + }, + identifier: [ + { + use: 'official', + value: '99c4dde5-3aca-4a4b-8b33-b50142e05da6', + }, + ], + status: 'active', + name: 'Brown Bag', + participant: [ + { + member: { + reference: 'Practitioner/137469', + }, + }, + ], + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: 'https://fhir.labs.smartregister.org:443/fhir/Practitioner/137469', + resource: { + resourceType: 'Practitioner', + id: '137469', + meta: { + versionId: '1', + lastUpdated: '2022-08-10T13:16:53.838+00:00', + source: '#2c648422b7a6e78e', + }, + identifier: [ + { + use: 'official', + value: '175ceaa4-0f75-4ab3-a8a7-413cc225f761', + }, + { + use: 'secondary', + value: 'b27939dd-4c8f-44c2-83dd-dc40e494f17d', + }, + ], + active: true, + name: [ + { + use: 'official', + family: 'Demo', + given: ['AeHIN'], + }, + ], + telecom: [ + { + system: 'email', + }, + ], + }, + search: { + mode: 'include', + }, + }, + ], +}; + +export const careTeam3500 = { + resourceType: 'Bundle', + id: '964d0cdc-3297-41fa-991f-47d639ff7635', + meta: { + lastUpdated: '2023-02-10T08:22:04.594+00:00', + }, + type: 'searchset', + total: 1, + link: [ + { + relation: 'self', + url: 'https://fhir.labs.smartregister.org/fhir/CareTeam/_search?_id=3500&_include=CareTeam%3A*', + }, + ], + entry: [ + { + fullUrl: 'https://fhir.labs.smartregister.org/fhir/CareTeam/3500', + resource: { + resourceType: 'CareTeam', + id: '3500', + meta: { + versionId: '1', + lastUpdated: '2021-10-12T07:29:44.733+00:00', + source: '#9837ac48046ef77c', + }, + text: { + status: 'generated', + div: 'Care Team', + }, + contained: [ + { + resourceType: 'Practitioner', + id: '3457', + name: [ + { + family: 'Careful', + given: ['Adam'], + prefix: ['Dr'], + }, + ], + }, + ], + status: 'active', + category: [ + { + coding: [ + { + system: 'http://loinc.org', + code: 'LA27976-2', + display: 'Encounter-focused care team', + }, + ], + }, + ], + name: 'Peter James Charlmers Care team', + subject: { + reference: 'Patient/3455', + display: 'Peter James Chalmers', + }, + encounter: { + reference: 'Encounter/3458', + }, + period: { + end: '2013-01-01', + }, + participant: [ + { + role: [ + { + text: 'responsiblePerson', + }, + ], + member: { + reference: 'Patient/3455', + display: 'Peter James Chalmers', + }, + }, + { + role: [ + { + text: 'adviser', + }, + ], + member: { + reference: '#pr1', + display: 'Dorothy Dietition', + }, + onBehalfOf: { + reference: 'Organization/0000', + }, + period: { + end: '2013-01-01', + }, + }, + ], + managingOrganization: [ + { + reference: 'Organization/3461', + }, + ], + }, + search: { + mode: 'match', + }, + }, + { + fullUrl: 'https://fhir.labs.smartregister.org/fhir/Encounter/3458', + resource: { + resourceType: 'Encounter', + id: '3458', + meta: { + versionId: '2', + lastUpdated: '2021-10-25T06:53:54.230+00:00', + source: '#1350520584a57b46', + }, + text: { + status: 'generated', + div: 'Encounter with patient @example', + }, + status: 'finished', + class: { + system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode', + code: 'IMP', + display: 'inpatient encounter to check on Obsesity', + }, + subject: { + reference: 'Patient/3455', + }, + }, + search: { + mode: 'include', + }, + }, + { + fullUrl: 'https://fhir.labs.smartregister.org/fhir/Patient/3455', + resource: { + resourceType: 'Patient', + id: '3455', + meta: { + versionId: '3', + lastUpdated: '2021-10-22T13:49:19.121+00:00', + source: '#70532eaf6e0ba7df', + }, + text: { + status: 'generated', + div: 'Peter James CHALMERS Address534 Erewhon St PleasantVille Vic Date of birth25 December 1974', + }, + active: true, + name: [ + { + use: 'official', + family: 'Chalmers', + given: ['Peter', 'James'], + }, + { + use: 'usual', + given: ['Jim'], + }, + { + use: 'maiden', + family: 'Windsor', + given: ['Peter', 'James'], + period: { + end: '2002', + }, + }, + ], + telecom: [ + { + use: 'home', + }, + { + system: 'phone', + value: '(03) 5555 6473', + use: 'work', + rank: 1, + }, + { + system: 'phone', + value: '(03) 3410 5613', + use: 'mobile', + rank: 2, + }, + { + system: 'phone', + value: '(03) 5555 8834', + use: 'old', + period: { + end: '2014', + }, + }, + ], + gender: 'male', + birthDate: '1974-12-25', + deceasedBoolean: false, + address: [ + { + use: 'home', + type: 'both', + text: '534 Erewhon St PeasantVille, Rainbow, Vic 3999', + line: ['534 Erewhon St'], + city: 'PleasantVille', + district: 'Rainbow', + state: 'Vic', + postalCode: '3999', + period: { + start: '1974-12-25', + }, + }, + ], + contact: [ + { + relationship: [ + { + coding: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v2-0131', + code: 'N', + }, + ], + }, + ], + name: { + family: 'du Marché', + _family: { + extension: [ + { + url: 'http://hl7.org/fhir/StructureDefinition/humanname-own-prefix', + valueString: 'VV', + }, + ], + }, + given: ['Bénédicte'], + }, + telecom: [ + { + system: 'phone', + value: '+33 (237) 998327', + }, + ], + address: { + use: 'home', + type: 'both', + line: ['534 Erewhon St'], + city: 'PleasantVille', + district: 'Rainbow', + state: 'Vic', + postalCode: '3999', + period: { + start: '1974-12-25', + }, + }, + gender: 'female', + period: { + start: '2012', + }, + }, + ], + managingOrganization: { + reference: 'Organization/3454', + }, + }, + search: { + mode: 'include', + }, + }, + ], +}; diff --git a/packages/fhir-import/src/containers/ImportDetailOverView/tests/index.test.tsx b/packages/fhir-import/src/containers/ImportDetailOverView/tests/index.test.tsx new file mode 100644 index 000000000..0693dc5a3 --- /dev/null +++ b/packages/fhir-import/src/containers/ImportDetailOverView/tests/index.test.tsx @@ -0,0 +1,155 @@ +import { store } from '@opensrp/store'; +import { authenticateUser } from '@onaio/session-reducer'; +import React from 'react'; +import { Router } from 'react-router'; +import { QueryClientProvider } from 'react-query'; +import { ViewDetails } from '..'; +import { careTeam2, careTeam3500, careTeamWithIncluded } from './fixtures'; +import { createBrowserHistory } from 'history'; +import { createTestQueryClient } from '../../ListView/tests/utils'; +import nock from 'nock'; +import { cleanup, fireEvent, render, waitForElementToBeRemoved } from '@testing-library/react'; +import { createMemoryHistory } from 'history'; +import { URL_CARE_TEAM, careTeamResourceType } from '../../../constants'; + +const history = createBrowserHistory(); + +const testQueryClient = createTestQueryClient(); + +jest.mock('fhirclient', () => { + return jest.requireActual('fhirclient/lib/entry/browser'); +}); + +jest.mock('@opensrp/notifications', () => ({ + __esModule: true, + ...Object.assign({}, jest.requireActual('@opensrp/notifications')), +})); + +const props = { + fhirBaseURL: 'http://test.server.org', + careTeamId: '131411', +}; + +beforeAll(() => { + nock.disableNetConnect(); + store.dispatch( + authenticateUser( + true, + { + email: 'bob@example.com', + name: 'Bobbie', + username: 'RobertBaratheon', + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + { api_token: 'hunter2', oAuth2Data: { access_token: 'sometoken', state: 'abcde' } } + ) + ); +}); + +afterEach(() => { + nock.cleanAll(); + cleanup(); +}); + +afterAll(() => { + nock.enableNetConnect(); +}); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const AppWrapper = (props: any) => { + return ( + + + + + + ); +}; + +test('works correctly', async () => { + nock(props.fhirBaseURL) + .get(`/${careTeamResourceType}/_search`) + .query({ _id: '131411', _include: 'CareTeam:*' }) + .reply(200, careTeamWithIncluded); + + const { queryByText } = render(); + await waitForElementToBeRemoved(queryByText(/Fetching Care team/i)); + + // see view details contents + const keyValuePairs = document.querySelectorAll( + 'div[data-testid="key-value"] .singleKeyValue-pair__default' + ); + keyValuePairs.forEach((pair) => { + expect(pair).toMatchSnapshot(); + }); + + expect(nock.pendingMocks()).toEqual([]); +}); + +test('Closes on clicking cancel (X) ', async () => { + const history = createMemoryHistory(); + history.push(URL_CARE_TEAM); + + const localProps = { + ...props, + careTeamId: '142534', + }; + + nock(props.fhirBaseURL) + .get(`/${careTeamResourceType}/_search`) + .query({ _id: localProps.careTeamId, _include: 'CareTeam:*' }) + .reply(200, careTeam2); + + const { queryByText } = render(); + await waitForElementToBeRemoved(queryByText(/Fetching Care team/i)); + + // see view details contents + const keyValuePairs = document.querySelectorAll( + 'div[data-testid="key-value"] .singleKeyValue-pair__default' + ); + keyValuePairs.forEach((pair) => { + expect(pair).toMatchSnapshot(); + }); + + // simulate clicking on close button + const button = document.querySelector('.flex-right button'); + fireEvent.click(button); + + expect(history.location.pathname).toEqual('/admin/CareTeams'); +}); + +test('shows broken page if fhir api is down', async () => { + nock(props.fhirBaseURL) + .get(`/${careTeamResourceType}/_search`) + .query({ _id: props.careTeamId, _include: 'CareTeam:*' }) + .replyWithError('coughid'); + + const { getByText, queryByText } = render(); + await waitForElementToBeRemoved(queryByText(/Fetching Care team/i)); + + expect(getByText(/coughid/)).toBeInTheDocument(); +}); + +test('1157 - view details errors out for careTeam 3500', async () => { + const thisProps = { + ...props, + careTeamId: '3500', + }; + nock(props.fhirBaseURL) + .get(`/${careTeamResourceType}/_search`) + .query({ _id: '3500', _include: 'CareTeam:*' }) + .reply(200, careTeam3500); + + const { queryByText } = render(); + await waitForElementToBeRemoved(queryByText(/Fetching Care team/i)); + + // see view details contents + const keyValuePairs = document.querySelectorAll( + 'div[data-testid="key-value"] .singleKeyValue-pair__default' + ); + keyValuePairs.forEach((pair) => { + expect(pair).toMatchSnapshot(); + }); + + expect(nock.pendingMocks()).toEqual([]); +}); diff --git a/packages/fhir-import/src/containers/ImportListView/index.tsx b/packages/fhir-import/src/containers/ImportListView/index.tsx new file mode 100644 index 000000000..7fc22c0ae --- /dev/null +++ b/packages/fhir-import/src/containers/ImportListView/index.tsx @@ -0,0 +1,165 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import React from 'react'; +import { Helmet } from 'react-helmet'; +import { Row, Col, Button, Divider, Dropdown, Popconfirm, Tag, Space } from 'antd'; +import type { MenuProps } from 'antd'; +import { BodyLayout, OpenSRPService } from '@opensrp/react-utils'; +import { MoreOutlined, CloudUploadOutlined } from '@ant-design/icons'; +import { RouteComponentProps } from 'react-router'; +import { useHistory, Link } from 'react-router-dom'; +import { + FHIRServiceClass, + useSimpleTabularView, + BrokenPage, + SearchForm, + TableLayout, + useSearchParams, + viewDetailsQuery, +} from '@opensrp/react-utils'; +import { + DATA_IMPORT_LIST_URL, + DATA_IMPORT_DETAIL_URL, + DATA_IMPORT_CREATE_URL, + dataImportRQueryKey, + IMPORT_DOMAIN_URI +} from '../../constants'; +import { ImportDetailViewDetails } from '../ImportDetailOverView'; +import { Dictionary } from '@onaio/utils'; +import { sendErrorNotification, sendSuccessNotification } from '@opensrp/notifications'; +import { useTranslation } from '../../mls'; +import type { TFunction } from '@opensrp/i18n'; +import { ICareTeam } from '@smile-cdr/fhirts/dist/FHIR-R4/interfaces/ICareTeam'; +import { RbacCheck, useUserRole } from '@opensrp/rbac'; +import { useQuery } from 'react-query'; +import { customFetch } from 'opensrp-server-service/dist/types'; +import { WorkflowDescription, formatTimestamp } from '../../helpers/utils'; +import { SortOrder } from 'antd/es/table/interface'; +import { ImportStatusTag } from '../../components/statusTag' + + +// route params for care team pages +interface RouteParams { + workflowId: string | undefined; +} + +interface Props { } + +export type ImportListPropTypes = Props & RouteComponentProps; + + + + +/** + * Function which shows the list of all roles and their details + * + * @param {Object} props - UserRolesList component props + * @returns {Function} returns User Roles list display + */ +export const DataImportList: React.FC = (props: ImportListPropTypes) => { + const { t } = useTranslation(); + const history = useHistory(); + + const { sParams } = useSearchParams() + const resourceId = sParams.get(viewDetailsQuery) ?? undefined; + + const { data, isFetching, isLoading, error } = useQuery(dataImportRQueryKey, () => { + const service = new OpenSRPService("/$import", IMPORT_DOMAIN_URI) + return service.list().then(res => { + return res + }) + }) + + if (error && !data) { + return ; + } + + + // TODO - add sort + const columns = [ + { + title: t('Workflow Id'), + dataIndex: 'workflowId' as const, + }, + { + title: t('Resource upload'), + dataIndex: 'workflowType' as const, + }, + { + title: t('File name'), + dataIndex: 'filename' as const, + }, + { + title: t('status'), + dataIndex: 'status' as const, + render: (_: any) => { + return + } + }, + { + title: t('Date created'), + dataIndex: 'dateCreated' as const, + defaultSortOrder: 'descend' as const, + sortDirections: ['ascend' as const, 'descend' as const], + sorter: (a: any, b: any) => { + const diff = a.dateCreated - b.dateCreated + return diff === 0 ? 0 : diff > 0 ? 1 : -1 + }, + render: (_: any) => formatTimestamp(_), + }, + { + title: t('Actions'), + width: '10%', + + // eslint-disable-next-line react/display-name + render: (_: unknown, record: WorkflowDescription) => ( + + + <> + + {t('View')} + + > + + + ), + }, + ]; + + const tableProps = { + datasource: (data ?? []), + columns, + loading: isFetching || isLoading, // TODO - add pagination + }; + const pageTitle = t('Data imports'); + const headerProps = { + pageHeaderProps: { + title: pageTitle, + onBack: undefined, + }, + }; + + return ( + + + {pageTitle} + + + + + + + + history.push(DATA_IMPORT_CREATE_URL)}> + + {t('Bulk upload')} + + + + + + + {resourceId && } + + + ); +}; diff --git a/packages/fhir-import/src/containers/ImportListView/tests/__snapshots__/index-rtl.test.tsx.snap b/packages/fhir-import/src/containers/ImportListView/tests/__snapshots__/index-rtl.test.tsx.snap new file mode 100644 index 000000000..b10ae19dc --- /dev/null +++ b/packages/fhir-import/src/containers/ImportListView/tests/__snapshots__/index-rtl.test.tsx.snap @@ -0,0 +1,301 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Care Teams list view renders correctly: table row 1 page 1 1`] = ` + + Care Team One + +`; + +exports[`Care Teams list view renders correctly: table row 1 page 1 2`] = ` + + + + Edit + + + + + + + + + +`; + +exports[`Care Teams list view renders correctly: table row 2 page 1 1`] = ` + + Care Team Three + +`; + +exports[`Care Teams list view renders correctly: table row 2 page 1 2`] = ` + + + + Edit + + + + + + + + + +`; + +exports[`Care Teams list view renders correctly: table row 3 page 1 1`] = ` + + Care Team Five + +`; + +exports[`Care Teams list view renders correctly: table row 3 page 1 2`] = ` + + + + Edit + + + + + + + + + +`; + +exports[`Care Teams list view renders correctly: table row 4 page 1 1`] = ` + + test 33 + +`; + +exports[`Care Teams list view renders correctly: table row 4 page 1 2`] = ` + + + + Edit + + + + + + + + + +`; + +exports[`Care Teams list view renders correctly: table row 5 page 1 1`] = ` + + Care Team Six + +`; + +exports[`Care Teams list view renders correctly: table row 5 page 1 2`] = ` + + + + Edit + + + + + + + + + +`; + +exports[`Care Teams list view renders correctly: table row 6 page 1 1`] = ` + + Care Team Four + +`; + +exports[`Care Teams list view renders correctly: table row 6 page 1 2`] = ` + + + + Edit + + + + + + + + + +`; diff --git a/packages/fhir-import/src/containers/ImportListView/tests/__snapshots__/index.test.tsx.snap b/packages/fhir-import/src/containers/ImportListView/tests/__snapshots__/index.test.tsx.snap new file mode 100644 index 000000000..f4f53c80d --- /dev/null +++ b/packages/fhir-import/src/containers/ImportListView/tests/__snapshots__/index.test.tsx.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Care Teams list view renders correctly: table row 1 page 1 1`] = ` + + + + + + + + + + + + + + + No data + + + +`; diff --git a/packages/fhir-import/src/containers/ImportListView/tests/fixtures.tsx b/packages/fhir-import/src/containers/ImportListView/tests/fixtures.tsx new file mode 100644 index 000000000..ca4e207cd --- /dev/null +++ b/packages/fhir-import/src/containers/ImportListView/tests/fixtures.tsx @@ -0,0 +1,13 @@ +export const workflows = [ { + "workflowId": "26aae779-0e6f-482d-82c3-a0fad1fd3689_orgToLocationAssignment", + "status": "completed", + "workflowType": "orgToLocationAssignment", + "dateCreated": 1720096011433, + "dateStarted": 1720096011435, + "dateEnded": 1720096013898, + "statusReason": { + "stdout": "Progress::Reading csv \n", + "stderr": "Start time: 15:26:53\nStarting csv import...\nReading csv file\nReturning records from csv file\nUnsupported request!\n{ \"final-response\": }\nEnd time: 15:26:53\nTotal time: 0.002815 seconds\n" + }, + "filename": "organizations_locations.csv" +}] \ No newline at end of file diff --git a/packages/fhir-import/src/containers/ImportListView/tests/index.test.tsx b/packages/fhir-import/src/containers/ImportListView/tests/index.test.tsx new file mode 100644 index 000000000..dd7864788 --- /dev/null +++ b/packages/fhir-import/src/containers/ImportListView/tests/index.test.tsx @@ -0,0 +1,131 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { authenticateUser } from '@onaio/session-reducer'; +import { DataImportList } from '..'; +import { Route, Router, Switch } from 'react-router'; +import { createBrowserHistory } from 'history'; +import nock from 'nock'; +import * as reactQuery from 'react-query'; +import { waitForElementToBeRemoved, fireEvent, render, cleanup } from '@testing-library/react'; +import { store } from '@opensrp/store'; +import { workflows } from './fixtures'; +import { DATA_IMPORT_LIST_URL, IMPORT_DOMAIN_URI } from '../../../constants'; +import { createMemoryHistory } from 'history'; +import { RoleContext } from '@opensrp/rbac'; +import { superUserRole } from '@opensrp/react-utils'; + + +const { QueryClient, QueryClientProvider } = reactQuery; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + cacheTime: 0, + }, + }, +}); + +jest.mock('@opensrp/notifications', () => ({ + __esModule: true, + ...Object.assign({}, jest.requireActual('@opensrp/notifications')), +})); + +const history = createBrowserHistory(); + +const props = { +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const AppWrapper = (props: any) => { + return ( + + + + + + {(routeProps) => } + + + {(routeProps) => } + + + + + + ); +}; + +describe('Care Teams list view', () => { + beforeAll(() => { + nock.disableNetConnect(); + store.dispatch( + authenticateUser( + true, + { + email: 'bob@example.com', + name: 'Bobbie', + username: 'RobertBaratheon', + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + { api_token: 'hunter2', oAuth2Data: { access_token: 'sometoken', state: 'abcde' } } + ) + ); + }); + afterAll(() => { + nock.enableNetConnect(); + }); + + afterEach(() => { + jest.resetAllMocks(); + jest.clearAllMocks(); + jest.restoreAllMocks(); + cleanup(); + }); + + it('renders correctly', async () => { + nock(IMPORT_DOMAIN_URI) + .get(`/$import`) + .reply(200, workflows) + .persist(); + + const history = createMemoryHistory(); + history.push(DATA_IMPORT_LIST_URL); + + render( + + + + ); + + await waitForElementToBeRemoved(document.querySelector('.ant-spin')); + + expect(document.querySelector('title')).toMatchInlineSnapshot(` + + Data imports + + `); + + document.querySelectorAll('tr').forEach((tr, idx) => { + tr.querySelectorAll('td').forEach((td) => { + expect(td).toMatchSnapshot(`table row ${idx} page 1`); + }); + }); + + // view details + nock(IMPORT_DOMAIN_URI) + .get(`/$import/${workflows?.[0].workflowId}`) + .query({ _id: '308', _include: 'CareTeam:*' }) + .reply(200, workflows[0]) + .persist(); + + // target the initial row view details + const dropdown = document.querySelector( + 'tbody tr:nth-child(1) [data-testid="action-dropdown"]' + ) as Element; + fireEvent.click(dropdown); + + expect(history.location).toEqual({}) + + }); +}); diff --git a/packages/fhir-import/src/containers/ImportListView/tests/utils.tsx b/packages/fhir-import/src/containers/ImportListView/tests/utils.tsx new file mode 100644 index 000000000..0d80e1256 --- /dev/null +++ b/packages/fhir-import/src/containers/ImportListView/tests/utils.tsx @@ -0,0 +1,45 @@ +/* eslint-disable react/display-name */ +import { render } from '@testing-library/react'; +import * as React from 'react'; +import { QueryClient, QueryClientProvider } from 'react-query'; + +export const createTestQueryClient = () => + new QueryClient({ + defaultOptions: { + queries: { + retry: false, + cacheTime: 0, + staleTime: 0, + }, + }, + }); + +/** + * Test util function to render element with react query provider + * + * @param ui - element to be rendered + * @returns {React.ReactElement} - returns element with react query provider wrapper + */ +export function renderWithClient(ui: React.ReactElement) { + const testQueryClient = createTestQueryClient(); + const { rerender, ...result } = render( + {ui} + ); + return { + ...result, + rerender: (rerenderUi: React.ReactElement) => + rerender({rerenderUi}), + }; +} + +/** + * Test Util function to create element wrapper + * + * @returns {React.ReactElement} - returns element with react query provider wrapper + */ +export function createWrapper() { + const testQueryClient = createTestQueryClient(); + return ({ children }: { children: React.ReactNode }) => ( + {children} + ); +} diff --git a/packages/fhir-import/src/containers/StartImportView/form.css b/packages/fhir-import/src/containers/StartImportView/form.css new file mode 100644 index 000000000..6c4a4337f --- /dev/null +++ b/packages/fhir-import/src/containers/StartImportView/form.css @@ -0,0 +1,6 @@ +form.import-form .ant-form-item-label label { + width: 144px; + word-break: break-word; + text-align: left; + white-space: wrap; + } \ No newline at end of file diff --git a/packages/fhir-import/src/containers/StartImportView/form.tsx b/packages/fhir-import/src/containers/StartImportView/form.tsx new file mode 100644 index 000000000..abe69c409 --- /dev/null +++ b/packages/fhir-import/src/containers/StartImportView/form.tsx @@ -0,0 +1,218 @@ +import React from 'react'; +import { Button, Form, Typography, UploadFile, Upload, Space } from 'antd'; +import UploadIcon from "@2fd/ant-design-icons/lib/Upload"; +import UploadOutlined from "@2fd/ant-design-icons/lib/UploadOutline"; +import { UploadChangeParam } from 'antd/es/upload'; +import { useHistory } from 'react-router'; +import { useQuery, useMutation, useQueryClient } from 'react-query'; +import { OpenSRPService, formItemLayout, tailLayout } from "@opensrp/react-utils"; +import { locations, users, organizations, careteams, inventories, orgToLocationAssignment, userToOrganizationAssignment, products, productImages, DATA_IMPORT_LIST_URL, IMPORT_DOMAIN_URI, dataImportRQueryKey } from '../../constants'; +import { useTranslation } from '../../mls'; +import { sendErrorNotification, sendSuccessNotification, sendInfoNotification } from '@opensrp/notifications'; +import FormItem from 'antd/es/form/FormItem'; +import { HTTPMethod, getDefaultHeaders } from '@opensrp/server-service'; +import "./form.css"; + +const { Text, Title } = Typography; + +interface DataImportFormProps { + hidden: string[] +} + +interface FormFields { + [locations]: UploadFile[]; + [users]: UploadFile[]; + [organizations]: UploadFile[]; + [careteams]: UploadFile[]; + [inventories]: UploadFile[]; + [orgToLocationAssignment]: UploadFile[]; + [userToOrganizationAssignment]: UploadFile[]; + [products]: UploadFile[]; + [productImages]: UploadFile[]; +} + +/** + * get payload for fetch + * + * @param {object} _ - signal object that allows you to communicate with a DOM request + * @param {string} accessToken - the access token + * @param {string} method - the HTTP method + * @param {object} data - data to be used for payload + * @returns {Object} the payload + */ +export function customFetchOptions( + _: AbortSignal, + accessToken: string, + method: HTTPMethod, + data?: T +): RequestInit { + const headers = getDefaultHeaders(accessToken) + return { + headers: { authorization: headers.authorization as string }, + method, + ...(data ? { body: data as any } : {}), + }; +} + +export const DataImportForm = (props: DataImportFormProps) => { + const { hidden } = props + const queryClient = useQueryClient(); + const history = useHistory(); + const { t } = useTranslation(); + const goTo = (url = '#') => history.push(url); + + const { mutate, isLoading } = useMutation( + async (values: FormFields) => { + const service = new OpenSRPService("/$import", IMPORT_DOMAIN_URI, customFetchOptions) + const formData = new FormData() + + Object.entries(values).forEach(([key, value]) => { + + if (value) { + formData.append(key, value?.[0]?.originFileObj) + } + }) + + service.create(formData) + }, + { + onError: (err: Error) => { + sendErrorNotification(err.message); + }, + onSuccess: async () => { + sendSuccessNotification(t('Data import started successfully')); + queryClient.invalidateQueries(dataImportRQueryKey) + goTo(DATA_IMPORT_LIST_URL); + }, + } + + ); + + + const formItems = [{ + formFieldName: users, + label: "Users", + UploadBtnText: "Attach users file" + }, { + formFieldName: locations, + label: "Locations", + UploadBtnText: "Attach locations file" + }, { + formFieldName: organizations, + label: "Organizations", + UploadBtnText: "Attach organizations file" + }, { + formFieldName: careteams, + label: "Careteams", + UploadBtnText: "Attach careteams file" + }, { + formFieldName: orgToLocationAssignment, + label: "Organization location assignment", + UploadBtnText: "Attach assignment file" + }, { + formFieldName: userToOrganizationAssignment, + label: "User organization assignment", + UploadBtnText: "Attach assignment file" + }, { + formFieldName: inventories, + label: "Inventory", + UploadBtnText: "Attach inventory file" + }] + + return + Select files to upload + Supported file formats CSV + { + mutate(values); + }} + > + { + formItems.map(item => { + const { formFieldName, label, UploadBtnText } = item + return + false} + accept="text/csv" + multiple={false} + maxCount={1} + > + }>{UploadBtnText} + + + }) + } + + false} + accept="text/csv" + multiple={false} + maxCount={1} + > + }>{"Attach product file"} + + {/* + + false} + accept="text/csv" + multiple={false} + maxCount={1} + > + }>{"Attach products file"} + + */} + + + + + {isLoading ? t('Uploading') : t('Start Import')} + + + + + + +} + + + + +// TODO - dry out +/** + * extract file from an input event + * + * @param e - event after a file upload + */ +export const normalizeFileInputEvent = (e: UploadChangeParam) => { + if (Array.isArray(e)) { + return e; + } + + return e.fileList; +}; \ No newline at end of file diff --git a/packages/fhir-import/src/containers/StartImportView/formInstructions.tsx b/packages/fhir-import/src/containers/StartImportView/formInstructions.tsx new file mode 100644 index 000000000..ffdd3c1f2 --- /dev/null +++ b/packages/fhir-import/src/containers/StartImportView/formInstructions.tsx @@ -0,0 +1,74 @@ +import React from 'react' +import { Typography, Steps, Button, Space } from 'antd'; +import UploadIcon from '@2fd/ant-design-icons/lib/Upload' +import ArrowDownThick from '@2fd/ant-design-icons/lib/ArrowDownThick' +import { OpenSRPService, downloadFile, getFileNameFromCDHHeader } from '@opensrp/react-utils'; +import {IMPORT_DOMAIN_URI} from '../../constants'; + +const { Title, Text } = Typography; + + +export const ImporterFormInstructions = () => { + return <> + + Step by step guide for bulk upload + + Follow these simple instructions to help you prepare, upload, and verify your data smoothly and efficiently + + + + + {InstructionStepOneTitle} + {InstructionStepOne} + + + + + {InstructionStepTwoTitle} + {InstructionStepTwo} + + + + {/* + */} + + > +} + +export const InstructionStepOneTitle = Prepare your data file +export const InstructionStepOne = + + Click the button below to download the bulk upload template file(s) + { + const service = new OpenSRPService("/$import/templates", IMPORT_DOMAIN_URI) + const response = await service.download(); + + // get filename from content-disposition header + const contentDispositionHeader = response.headers.get('content-disposition'); + const fileName = contentDispositionHeader + ? getFileNameFromCDHHeader(contentDispositionHeader) + : `import-template`; + + // get blob data from response + const blob = await response.blob(); + + downloadFile(blob, fileName); + service.list().then(res => { + + }) + }}>Download Template + Enter your data into the template file. Ensure all required fields are filled and follow the specified format(e.g. date format,) + Check for any data inconsistencies or errors (e.g. missing values, incorrect data types) before uploading + + + +export const InstructionStepTwoTitle = Upload your data file +export const InstructionStepTwo = + + Click the "Attach" button to select your prepared data file. + Once the file or files are selected, click "Start Import" to begin the upload + + diff --git a/packages/fhir-import/src/containers/StartImportView/index.tsx b/packages/fhir-import/src/containers/StartImportView/index.tsx new file mode 100644 index 000000000..1186371a0 --- /dev/null +++ b/packages/fhir-import/src/containers/StartImportView/index.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import { Helmet } from 'react-helmet'; +import { ImporterFormInstructions } from './formInstructions'; +import { BodyLayout } from '@opensrp/react-utils'; +import { Row, Col } from 'antd' +import { DataImportForm } from './form'; + + +export const StartDataImport = () => { + const pageTitle = "Data imports" + const headerProps = { + pageHeaderProps: { + title: pageTitle, + onBack: undefined, + }, + }; + return <> + + + {pageTitle} + + + + + + + + + > +} diff --git a/packages/fhir-import/src/helpers/utils.tsx b/packages/fhir-import/src/helpers/utils.tsx new file mode 100644 index 000000000..50a565927 --- /dev/null +++ b/packages/fhir-import/src/helpers/utils.tsx @@ -0,0 +1,23 @@ +export interface WorkflowDescription { + "workflowId": string; + "status": string; + "workflowType": string; + "dateStarted": number; + "dateEnded": number; + "dateCreated": number; + "statusReason"?: any; + filename: string; +} + + +export function formatTimestamp(timestamp: number, locale = 'en-US') { + const date = new Date(timestamp); + return date.toLocaleString(locale, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }); +} \ No newline at end of file diff --git a/packages/fhir-import/src/index.d.ts b/packages/fhir-import/src/index.d.ts new file mode 100644 index 000000000..1fbea2e91 --- /dev/null +++ b/packages/fhir-import/src/index.d.ts @@ -0,0 +1,144 @@ +declare module '@2fd/ant-design-icons' { + import { ReactNode, SVGProps } from 'react'; + + export interface IconProps extends SVGProps { + title?: string; + size?: number | string; + color?: string; + } + + export interface IconComponent { + (props: IconProps): ReactNode; + } + + export const AccountBookOutlined: IconComponent; + export const AlertOutlined: IconComponent; + export const AliyunOutlined: IconComponent; + export const AmazonOutlined: IconComponent; + export const AndroidOutlined: IconComponent; + export const AppleOutlined: IconComponent; + export const AppstoreOutlined: IconComponent; + export const ArrowDownOutlined: IconComponent; + export const ArrowLeftOutlined: IconComponent; + export const ArrowRightOutlined: IconComponent; + export const ArrowUpOutlined: IconComponent; + export const AudioOutlined: IconComponent; + export const BackwardOutlined: IconComponent; + export const BankOutlined: IconComponent; + export const BarcodeOutlined: IconComponent; + export const BarsOutlined: IconComponent; + export const BellOutlined: IconComponent; + export const BookOutlined: IconComponent; + export const BoxPlotOutlined: IconComponent; + export const BugOutlined: IconComponent; + export const BuildOutlined: IconComponent; + export const BulbOutlined: IconComponent; + export const CalculatorOutlined: IconComponent; + export const CalendarOutlined: IconComponent; + export const CameraOutlined: IconComponent; + export const CarOutlined: IconComponent; + export const CarryOutOutlined: IconComponent; + export const CheckCircleOutlined: IconComponent; + export const CheckSquareOutlined: IconComponent; + export const ClockCircleOutlined: IconComponent; + export const CloudOutlined: IconComponent; + export const CodeOutlined: IconComponent; + export const CoffeeOutlined: IconComponent; + export const CompassOutlined: IconComponent; + export const ContactsOutlined: IconComponent; + export const ContainerOutlined: IconComponent; + export const ControlOutlined: IconComponent; + export const CreditCardOutlined: IconComponent; + export const CrownOutlined: IconComponent; + export const CustomerServiceOutlined: IconComponent; + export const DashboardOutlined: IconComponent; + export const DatabaseOutlined: IconComponent; + export const DeleteOutlined: IconComponent; + export const DesktopOutlined: IconComponent; + export const DislikeOutlined: IconComponent; + export const DollarOutlined: IconComponent; + export const DownloadOutlined: IconComponent; + export const EditOutlined: IconComponent; + export const EnvironmentOutlined: IconComponent; + export const ExclamationCircleOutlined: IconComponent; + export const ExperimentOutlined: IconComponent; + export const ExportOutlined: IconComponent; + export const EyeOutlined: IconComponent; + export const FileOutlined: IconComponent; + export const FilterOutlined: IconComponent; + export const FireOutlined: IconComponent; + export const FlagOutlined: IconComponent; + export const FolderOutlined: IconComponent; + export const FolderOpenOutlined: IconComponent; + export const FundProjectionScreenOutlined: IconComponent; + export const FunnelPlotOutlined: IconComponent; + export const GiftOutlined: IconComponent; + export const GlobalOutlined: IconComponent; + export const GoldOutlined: IconComponent; + export const HddOutlined: IconComponent; + export const HeartOutlined: IconComponent; + export const HomeOutlined: IconComponent; + export const HourglassOutlined: IconComponent; + export const IdcardOutlined: IconComponent; + export const InboxOutlined: IconComponent; + export const KeyOutlined: IconComponent; + export const LaptopOutlined: IconComponent; + export const LayoutOutlined: IconComponent; + export const LikeOutlined: IconComponent; + export const LineChartOutlined: IconComponent; + export const LockOutlined: IconComponent; + export const MailOutlined: IconComponent; + export const MedicineBoxOutlined: IconComponent; + export const MehOutlined: IconComponent; + export const MessageOutlined: IconComponent; + export const MobileOutlined: IconComponent; + export const MoneyCollectOutlined: IconComponent; + export const NotificationOutlined: IconComponent; + export const OrderedListOutlined: IconComponent; + export const PaperClipOutlined: IconComponent; + export const PartitionOutlined: IconComponent; + export const PauseOutlined: IconComponent; + export const PhoneOutlined: IconComponent; + export const PictureOutlined: IconComponent; + export const PlayCircleOutlined: IconComponent; + export const PlusOutlined: IconComponent; + export const PrinterOutlined: IconComponent; + export const ProfileOutlined: IconComponent; + export const ProjectOutlined: IconComponent; + export const PushpinOutlined: IconComponent; + export const QrcodeOutlined: IconComponent; + export const ReadOutlined: IconComponent; + export const ReconciliationOutlined: IconComponent; + export const RedEnvelopeOutlined: IconComponent; + export const ReloadOutlined: IconComponent; + export const RestOutlined: IconComponent; + export const RocketOutlined: IconComponent; + export const SafetyCertificateOutlined: IconComponent; + export const SaveOutlined: IconComponent; + export const ScheduleOutlined: IconComponent; + export const SecurityScanOutlined: IconComponent; + export const SettingOutlined: IconComponent; + export const ShopOutlined: IconComponent; + export const ShoppingCartOutlined: IconComponent; + export const SkinOutlined: IconComponent; + export const SmileOutlined: IconComponent; + export const SolutionOutlined: IconComponent; + export const StarOutlined: IconComponent; + export const StopOutlined: IconComponent; + export const SwitcherOutlined: IconComponent; + export const TabletOutlined: IconComponent; + export const TagOutlined: IconComponent; + export const TagsOutlined: IconComponent; + export const ToolOutlined: IconComponent; + export const UnlockOutlined: IconComponent; + export const UsergroupAddOutlined: IconComponent; + export const UsergroupDeleteOutlined: IconComponent; + export const UserOutlined: IconComponent; + export const VideoCameraOutlined: IconComponent; + export const WalletOutlined: IconComponent; + export const WarningOutlined: IconComponent; + export const WifiOutlined: IconComponent; + export const ZoomInOutlined: IconComponent; + export const ZoomOutOutlined: IconComponent; + } + \ No newline at end of file diff --git a/packages/fhir-import/src/index.tsx b/packages/fhir-import/src/index.tsx new file mode 100644 index 000000000..b5f8a0c0a --- /dev/null +++ b/packages/fhir-import/src/index.tsx @@ -0,0 +1,5 @@ +export * from "./containers/ImportListView"; +export * from "./constants"; +export * from "./containers/StartImportView" +export * from "./containers/ImportDetailOverView" + diff --git a/packages/fhir-import/src/mls.ts b/packages/fhir-import/src/mls.ts new file mode 100644 index 000000000..35ae0f9c7 --- /dev/null +++ b/packages/fhir-import/src/mls.ts @@ -0,0 +1,8 @@ +import { useTranslation as useOrigTranslation } from '@opensrp/i18n'; +import type { UseTranslationOptions } from '@opensrp/i18n'; + +export const namespace = 'fhir-views'; + +export const useTranslation = (ns?: string, options?: UseTranslationOptions) => { + return useOrigTranslation(ns ? ns : namespace, options); +}; diff --git a/packages/fhir-import/tsconfig.json b/packages/fhir-import/tsconfig.json new file mode 100644 index 000000000..ec7a51e41 --- /dev/null +++ b/packages/fhir-import/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "declarationDir": "dist/types" + }, + "include": ["src"], + "exclude": ["**/node_modules", "**/coverage", "**/dist", "**/*.spec.ts", "**/*tests"] +} diff --git a/packages/rbac/src/adapters/keycloakAdapter.ts b/packages/rbac/src/adapters/keycloakAdapter.ts index 85636eec1..c1bf988cb 100644 --- a/packages/rbac/src/adapters/keycloakAdapter.ts +++ b/packages/rbac/src/adapters/keycloakAdapter.ts @@ -1,4 +1,4 @@ -import { AuthZResource, FhirResource, FhirResources, Permit } from '../constants'; +import { AuthZResource, FhirResource, KeycloakDefinedResource, KeycloakDefinedResources, Permit } from '../constants'; import { RbacAdapter } from '../helpers/types'; import { UserRole } from '../roleDefinition'; @@ -10,8 +10,8 @@ const fhirVerbToPermitLookup: Record = { MANAGE: Permit.MANAGE, }; -const getFhirResourceString = (rawResourceString: string): FhirResource | undefined => { - const matchedResource = FhirResources.filter( +const getFhirResourceString = (rawResourceString: string): KeycloakDefinedResource | undefined => { + const matchedResource = KeycloakDefinedResources.filter( (resource) => resource.toUpperCase() === rawResourceString.toUpperCase() ); return matchedResource[0]; diff --git a/packages/rbac/src/constants.ts b/packages/rbac/src/constants.ts index 654c63445..bb296bb42 100644 --- a/packages/rbac/src/constants.ts +++ b/packages/rbac/src/constants.ts @@ -44,7 +44,19 @@ export const FhirResources = [ ] as const; export type FhirResource = typeof FhirResources[number]; -export type AuthZResource = IamResource | FhirResource; +/** Roles for Situations where we have views that are not directly tied to any of the native fhir resources. + * These are custom and only relevant for the web. These should also be defined and parsed in a similar design as + * FhirResources + */ +export const WebCustomResources = [ + 'WebDataImport' +] as const; +export type WebCustomResource = typeof WebCustomResources[number] + +export const KeycloakDefinedResources = [...FhirResources, ...WebCustomResources] as const +export type KeycloakDefinedResource = typeof KeycloakDefinedResources[number] + +export type AuthZResource = IamResource | FhirResource | WebCustomResource; export type BinaryNumber = number; export type PermitKey = keyof typeof Permit; export type PermitKeyValues = Valueof; diff --git a/packages/rbac/src/helpers/utils.ts b/packages/rbac/src/helpers/utils.ts index 7c05d1fed..fba685668 100644 --- a/packages/rbac/src/helpers/utils.ts +++ b/packages/rbac/src/helpers/utils.ts @@ -1,10 +1,11 @@ import { AuthZResource, - FhirResources, + KeycloakDefinedResources, IamResources, Permit, PermitKey, ResourcePermitMap, + WebCustomResources, } from '../constants'; import invariant from 'invariant'; @@ -21,7 +22,8 @@ export function makeArray(obj: T | T[]): T[] { return asArray; } -const lowecasedAuthZResourceTags = [...IamResources, ...FhirResources].map( +// TODO - this is not actually lowercased +const lowerCasedAuthZResources = [...IamResources, ...KeycloakDefinedResources, ...WebCustomResources].map( (tag) => tag // tag.toLowerCase() ) as string[]; @@ -40,7 +42,7 @@ export function validatePermissionStr(permission: string) { } const [resource, permit] = parts; // const resourceIsRecognized = lowecasedAuthZResourceTags.includes(resource.toLowerCase()); - const resourceIsRecognized = lowecasedAuthZResourceTags.includes(resource); + const resourceIsRecognized = lowerCasedAuthZResources.includes(resource); const permitIsRecognized = permitLiteralKeys.includes(permit.toLowerCase()); if (!resourceIsRecognized || !permitIsRecognized) { return false; diff --git a/yarn.lock b/yarn.lock index d79b02c16..53638ba2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3947,6 +3947,24 @@ __metadata: languageName: unknown linkType: soft +"@opensrp/fhir-import@workspace:packages/fhir-import": + version: 0.0.0-use.local + resolution: "@opensrp/fhir-import@workspace:packages/fhir-import" + dependencies: + "@2fd/ant-design-icons": ^2.6.0 + "@opensrp/notifications": ^0.0.5 + "@opensrp/rbac": "workspace:^" + "@opensrp/react-utils": ^0.0.12 + peerDependencies: + "@opensrp/i18n": ^0.0.1 + react: ^17.0.0 + react-dom: 17.0.0 + react-query: ^3.15.1 + react-router: ^5.2.0 + react-router-dom: ^5.2.0 + languageName: unknown + linkType: soft + "@opensrp/fhir-location-management@0.0.1, @opensrp/fhir-location-management@^0.0.1, @opensrp/fhir-location-management@workspace:packages/fhir-location-management": version: 0.0.0-use.local resolution: "@opensrp/fhir-location-management@workspace:packages/fhir-location-management"
+ {data.statusReason?.stdout} + {data.statusReason?.stderr} +