diff --git a/src/client-registry/client-registry-data/client-registry-actions/send-patient-to-cr-menu-item.component.tsx b/src/client-registry/client-registry-data/client-registry-actions/send-patient-to-cr-menu-item.component.tsx new file mode 100644 index 0000000..ebb38c1 --- /dev/null +++ b/src/client-registry/client-registry-data/client-registry-actions/send-patient-to-cr-menu-item.component.tsx @@ -0,0 +1,34 @@ +import React, { useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { Patient } from "../../../types"; +import { OverflowMenuItem } from "@carbon/react"; +import { showModal } from "@openmrs/esm-framework"; + +interface SendPatientToCRActionMenuProps { + patient: Patient; + closeModal: () => void; +} + +const SendPatientToCRActionMenu: React.FC = ({ + patient, +}) => { + const { t } = useTranslation(); + const launchSendPatientToCRModal = useCallback(() => { + const dispose = showModal("send-patient-to-cr-dialog", { + closeModal: () => dispose(), + patient, + }); + }, [patient]); + + return ( + + ); +}; + +export default SendPatientToCRActionMenu; diff --git a/src/client-registry/client-registry-data/client-registry-data.component.tsx b/src/client-registry/client-registry-data/client-registry-data.component.tsx new file mode 100644 index 0000000..e306775 --- /dev/null +++ b/src/client-registry/client-registry-data/client-registry-data.component.tsx @@ -0,0 +1,238 @@ +import React, { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + extractErrorMessagesFromResponse, + startClientRegistryTask, + usePatients, +} from "../client-registry.resource"; +import { + ExtensionSlot, + formatDate, + parseDate, + showNotification, + showSnackbar, + usePagination, +} from "@openmrs/esm-framework"; +import { + DataTable, + TableContainer, + Pagination, + Tile, + TableBody, + Table, + TableHead, + TableRow, + TableHeader, + TableCell, + DataTableSkeleton, + Button, + InlineLoading, +} from "@carbon/react"; +import styles from "./client-registry-data.scss"; +import { OverflowMenuVertical } from "@carbon/react/icons"; + +import OrderCustomOverflowMenuComponent from "../../ui-components/overflow-menu.component"; + +const ClientRegistryData: React.FC = () => { + const { t } = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + + const { patients, isLoading: loading, mutate } = usePatients("sarah", false); + + const pageSizes = [10, 20, 30, 40, 50]; + const [currentPageSize, setPageSize] = useState(10); + const { + goTo, + results: paginatedPatientEntries, + currentPage, + } = usePagination(patients, currentPageSize); + + const tableHeaders = useMemo( + () => [ + { id: 0, header: t("name", "Name"), key: "name" }, + { id: 1, header: t("identifiers", "Identifiers"), key: "identifiers" }, + { id: 2, header: t("gender", "Gender"), key: "gender" }, + { id: 3, header: t("age", "Age"), key: "age" }, + { id: 4, header: t("birthdate", "Birthdate"), key: "birthdate" }, + { + id: 5, + header: t("maritalStatus", "Marital Status"), + key: "maritalStatus", + }, + { id: 6, header: t("occupation", "Occupation"), key: "occupation" }, + { id: 7, header: t("nationality", "Nationality"), key: "nationality" }, + { id: 8, header: t("dead", "Dead"), key: "dead" }, + { id: 9, actions: t("actions", "Actions"), key: "actions" }, + ], + [t] + ); + + const startClientRegistry = async (e) => { + e.preventDefault(); + setIsLoading(true); + startClientRegistryTask().then( + () => { + mutate(); + setIsLoading(false); + showSnackbar({ + isLowContrast: true, + title: t("runTask", "Start Client Registry Task"), + kind: "success", + subtitle: t( + "successfullyStarted", + `You have successfully started Client Registry` + ), + }); + }, + (error) => { + const errorMessages = extractErrorMessagesFromResponse(error); + + mutate(); + setIsLoading(false); + showNotification({ + title: t( + `errorStartingTask', 'Error Starting client registry task"),` + ), + kind: "error", + critical: true, + description: errorMessages.join(", "), + }); + } + ); + }; + + const tableRows = useMemo(() => { + return patients?.map((patient, index) => ({ + ...patient, + id: patient?.uuid, + name: patient?.person?.display, + identifiers: patient.identifiers + .map((identifier) => identifier.display) + .join(", "), + gender: patient?.person?.gender, + age: patient?.person?.age, + birthdate: formatDate(parseDate(patient?.person?.birthdate)), + maritalStatus: + patient.attributes.find( + (attribute) => + attribute.attributeType?.uuid === + "8d871f2a-c2cc-11de-8d13-0010c6dffd0f" + )?.value?.display || "", + occupation: + patient.attributes.find( + (attribute) => + attribute.attributeType?.uuid === + "b0868a16-4f8e-43da-abfc-6338c9d8f56a" + )?.value || "", + nationality: + patient.attributes.find( + (attribute) => + attribute.attributeType?.uuid === + "dec484be-1c43-416a-9ad0-18bd9ef28929" + )?.value?.display || "", + dead: patient?.person?.dead ? "true" : "false", + actions: ( + + + + } + > + + + ), + })); + }, [patients]); + + if (loading) { + return ; + } + + return ( +
+ + + {({ + rows, + headers, + getHeaderProps, + getTableProps, + getRowProps, + getTableContainerProps, + }) => ( + + + + + {headers.map((header) => ( + + {header.header} + + ))} + + + + {rows.map((row) => ( + + {row.cells.map((cell) => ( + + {cell.value} + + ))} + + ))} + +
+ {rows.length === 0 ? ( +
+ +
+

+ {t("noPatientsToDisplay", "No patients to display")} +

+
+
+
+ ) : null} + { + if (pageSize !== currentPageSize) { + setPageSize(pageSize); + } + if (page !== currentPage) { + goTo(page); + } + }} + /> +
+ )} +
+
+ ); +}; + +export default ClientRegistryData; diff --git a/src/client-registry/client-registry-data/client-registry-data.scss b/src/client-registry/client-registry-data/client-registry-data.scss new file mode 100644 index 0000000..a8c3648 --- /dev/null +++ b/src/client-registry/client-registry-data/client-registry-data.scss @@ -0,0 +1,227 @@ +@use '@carbon/styles/scss/spacing'; +@use '@carbon/styles/scss/type'; +@use '@carbon/styles/scss/colors'; +@import "~@openmrs/esm-styleguide/src/vars"; +@import '../../root.scss'; + +title { + width: 6.938rem; + height: 1.75rem; + margin: 0.438rem 22.875rem 0.813rem 0; + font-family: IBMPlexSans; + font-size: 1.25rem; + font-weight: normal; + font-stretch: normal; + font-style: normal; + line-height: 1.4; + letter-spacing: normal; +} + +.link { + text-decoration: none; +} + +.breadcrumbsSlot { + grid-row: 1 / 2; + grid-column: 1 / 2; +} + +.orderTabsContainer { + height: 100%; + width: 100%; + + :global(.cds--tab-content) { + padding: 0 !important; + } +} + +.orderTabs { + grid-column: 'span 2'; + padding: 0 spacing.$spacing-05; +} + +.newListButton { + width: fit-content; + justify-self: end; + align-self: center; +} + +.tabsContainer { + background-color: $ui-02; + + :global(.cds--tabs__nav-item--selected) { + box-shadow: inset 0 2px 0 0 var(--brand-03) !important; + } + + :global(.cds--tab--list) button { + max-width: 12rem !important; + } +} + +.hiddenTabsContent, +.tabs .hiddenTabsContent { + display: none; +} + +.patientListTableContainer { + grid-row: 3 / 4; + grid-column: 1 / 2; + height: 100%; + margin: 0.5rem spacing.$spacing-05; + background-color: $ui-01; + border: 0.5px solid #e0e0e0; + + :global(.cds--data-table-container) { + padding-top: 0 !important; + } + + tbody>tr>:nth-child(2) { + white-space: nowrap; + } + + :global(.cds--data-table td) { + height: unset !important; + } + + :global(.cds--data-table--zebra) tbody tr[data-parent-row]:nth-child(4n + 1) td { + background-color: $ui-02; + border-bottom: 1px solid $ui-03; + border-top: 1px solid $ui-03; + } + + :global(.cds--data-table--zebra) tbody tr[data-parent-row]:nth-child(4n + 3) td { + background-color: $ui-01; + border-bottom: 1px solid $ui-03; + } +} + +.tableContainer { + background-color: $ui-01; + margin: 0 spacing.$spacing-05; + padding: 0; + + a { + text-decoration: none; + } + + th { + color: $text-02; + } + + :global(.cds--data-table) { + background-color: $ui-03; + } + + .toolbarContent { + height: spacing.$spacing-07; + margin-bottom: spacing.$spacing-02; + } +} + +.activePatientsTable tr:last-of-type { + td { + border-bottom: none; + } +} + +.searchContainer { + display: flex; + align-items: center; + flex-direction: row-reverse; + padding-top: 0.5rem; + + :global(.cds--search-magnifier-icon) { + z-index: 0 !important; + } + + input { + background-color: #fff; + } +} + +.addOrderBtn { + width: 10rem !important; + padding: 0.5rem !important; + margin-left: 1rem; + margin-right: 1rem; +} + +.patientSearch { + width: 25rem; + border-bottom-color: $ui-03; +} + +.locationFilter { + width: 25rem; +} + +.search { + width: 100%; + max-width: 16rem; + background-color: $ui-02; + border-bottom-color: $ui-03; +} + +.container { + background-color: $ui-01; +} + +.expandedLabQueueVisitRow { + :global(.cds--tab-content) { + padding: 0.5rem 0; + } + + td { + padding: 0.5rem; + + >div { + max-height: max-content !important; + background-color: $ui-02; + } + } + + th[colspan] td[colspan]>div:first-child { + padding: 0 1rem; + } +} + + +.tileContainer { + background-color: $ui-02; + border-top: 1px solid $ui-03; + padding: 5rem 0; +} + +.tile { + margin: auto; + width: fit-content; +} + +.tileContent { + display: flex; + flex-direction: column; + align-items: center; +} + +.content { + @include type.type-style('heading-compact-02'); + color: $text-02; + margin-bottom: 0.5rem; +} + +.testOrder { + display: flex; + flex-direction: column; + justify-content: space-between; + border: 1px solid colors.$gray-20; + margin-top: 1rem; + border-bottom: none; +} + +.testCell { + padding-left: spacing.$spacing-05 !important; +} + +.testType { + color: colors.$blue-60; +} diff --git a/src/client-registry/client-registry-data/client-registry-dialogs/send-patient-to-cr-dialog.component.tsx b/src/client-registry/client-registry-data/client-registry-dialogs/send-patient-to-cr-dialog.component.tsx new file mode 100644 index 0000000..313d641 --- /dev/null +++ b/src/client-registry/client-registry-data/client-registry-dialogs/send-patient-to-cr-dialog.component.tsx @@ -0,0 +1,129 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Patient } from "../../../types"; +import styles from "./send-patient-to-cr-dialog.scss"; +import { + Form, + ModalHeader, + ModalBody, + InlineLoading, + ModalFooter, + Button, +} from "@carbon/react"; +import { Payload, submitPatient } from "../../client-registry.resource"; +import { + showNotification, + showSnackbar, + useConfig, +} from "@openmrs/esm-framework"; + +interface SendPatientToCRDialogProps { + patient: Patient; + closeModal: () => void; +} + +const SendPatientToCRDialog: React.FC = ({ + patient, + closeModal, +}) => { + const { clientRegistryUrl } = useConfig(); + const apiUrl = `${clientRegistryUrl}/Patient`; + const { t } = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + + const crPatient: Payload = { + resourceType: "Patient", + identifier: [], + name: [], + telecom: [], + maritalStatus: undefined, + managingOrganization: undefined, + address: [], + contact: [], + communication: [], + extension: [], + birthDate: "", + deceasedBoolean: false, + active: false, + gender: "", + }; + + const sendPatientToCR = async (e) => { + e.preventDefault(); + setIsLoading(true); + // send patient to CR + submitPatient(apiUrl, crPatient).then( + () => { + setIsLoading(false); + showSnackbar({ + isLowContrast: true, + title: t("sendPatient", "Send Patient"), + kind: "success", + subtitle: t( + "successfullySent", + `You have successfully sent to Client Registry` + ), + }); + closeModal(); + }, + (error) => { + setIsLoading(false); + closeModal(); + showNotification({ + title: t( + `errorSendingPatient', 'Error Sending patient to client registry"),` + ), + kind: "error", + critical: true, + description: error.messages, + }); + } + ); + }; + + return ( + <> +
+ + + {isLoading && ( + + )} +
+

+ {patient.person?.display} : {patient.person.age} years +

+

+ {patient.identifiers + .map((identifier) => identifier.display) + .join(", ")} +

+
+
+ + + + + + + + ); +}; + +export default SendPatientToCRDialog; diff --git a/src/client-registry/client-registry-data/client-registry-dialogs/send-patient-to-cr-dialog.scss b/src/client-registry/client-registry-data/client-registry-dialogs/send-patient-to-cr-dialog.scss new file mode 100644 index 0000000..5a6388b --- /dev/null +++ b/src/client-registry/client-registry-data/client-registry-dialogs/send-patient-to-cr-dialog.scss @@ -0,0 +1,34 @@ +@use '@carbon/styles/scss/spacing'; +@use '@carbon/styles/scss/type'; +@import "~@openmrs/esm-styleguide/src/vars"; + +.checkbox { + margin: 10px; + font-size: 15px; + font-weight: bold; +} + +.valueWidget { + margin-left: 10px +} + +.section { + table { + font-family: Arial, sans-serif; + border-collapse: collapse; + width: 100%; + } + + td, + th { + border: 1px solid #000; + text-align: left; + font-size: 12px; + padding: 8px; + width: 80px; + } + + th { + background-color: #f2f2f2; + } +} \ No newline at end of file diff --git a/src/client-registry/client-registry-illustration.component.tsx b/src/client-registry/client-registry-illustration.component.tsx new file mode 100644 index 0000000..30c15d8 --- /dev/null +++ b/src/client-registry/client-registry-illustration.component.tsx @@ -0,0 +1,32 @@ +import React from "react"; + +const Illustration: React.FC = () => { + return ( + + + + + + + ); +}; + +export default Illustration; diff --git a/src/client-registry/client-registry-tiles/client-registry-summary-tiles.component.tsx b/src/client-registry/client-registry-tiles/client-registry-summary-tiles.component.tsx new file mode 100644 index 0000000..e4e0a43 --- /dev/null +++ b/src/client-registry/client-registry-tiles/client-registry-summary-tiles.component.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import styles from "./client-registry-summary-tiles.scss"; +import { + AssignedExtension, + useConnectedExtensions, + Extension, +} from "@openmrs/esm-framework"; +import { ComponentContext } from "@openmrs/esm-framework/src/internal"; + +const ClientRegistrySummaryTiles: React.FC = () => { + const clientRegistryTileSlot = "client-registry-tiles-slot"; + + const tilesExtensions = useConnectedExtensions( + clientRegistryTileSlot + ) as AssignedExtension[]; + + return ( +
+ {tilesExtensions + .filter((extension) => Object.keys(extension.meta).length > 0) + .map((extension) => { + return ( + + + + ); + })} +
+ ); +}; + +export default ClientRegistrySummaryTiles; diff --git a/src/client-registry/client-registry-tiles/client-registry-summary-tiles.scss b/src/client-registry/client-registry-tiles/client-registry-summary-tiles.scss new file mode 100644 index 0000000..986c616 --- /dev/null +++ b/src/client-registry/client-registry-tiles/client-registry-summary-tiles.scss @@ -0,0 +1,18 @@ +@use '@carbon/styles/scss/spacing'; +@use '@carbon/styles/scss/type'; +@import "~@openmrs/esm-styleguide/src/vars"; + +.cardContainer { + background-color: $ui-02 ; + display: flex; + padding: 0 spacing.$spacing-05 spacing.$spacing-10 spacing.$spacing-03; +} + +.cardContainer>div:nth-child(1), +.cardContainer>div:nth-child(2), +.cardContainer>div:nth-child(3), +.cardContainer>div:nth-child(4), +.cardContainer>div:nth-child(5), +{ +width: 20%; +} \ No newline at end of file diff --git a/src/client-registry/client-registry-tiles/client-registry-tile/client-registry-tile.component.tsx b/src/client-registry/client-registry-tiles/client-registry-tile/client-registry-tile.component.tsx new file mode 100644 index 0000000..8e974eb --- /dev/null +++ b/src/client-registry/client-registry-tiles/client-registry-tile/client-registry-tile.component.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { Tile } from "@carbon/react"; +import styles from "./client-registry-tile.scss"; +interface ClientRegistryTileProps { + label: string; + value: number; + headerLabel: string; + children?: React.ReactNode; +} + +const ClientRegistryTile: React.FC = ({ + label, + value, + headerLabel, + children, +}) => { + return ( + +
+
+ + {children} +
+
+
+
+ +

{value}

+
+
+ ); +}; + +export default ClientRegistryTile; diff --git a/src/client-registry/client-registry-tiles/client-registry-tile/client-registry-tile.scss b/src/client-registry/client-registry-tiles/client-registry-tile/client-registry-tile.scss new file mode 100644 index 0000000..2128b00 --- /dev/null +++ b/src/client-registry/client-registry-tiles/client-registry-tile/client-registry-tile.scss @@ -0,0 +1,43 @@ +@use '@carbon/styles/scss/spacing'; +@use '@carbon/styles/scss/type'; +@import "~@openmrs/esm-styleguide/src/vars"; + +.tileContainer { + border: 0.0625rem solid $ui-03; + flex-grow: 1; + height: 7.875rem; + padding: 0 spacing.$spacing-05; + margin: spacing.$spacing-03 spacing.$spacing-03; +} + +.tileHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: spacing.$spacing-03; +} + +.headerLabel { + @include type.type-style("heading-01"); + color: $text-02; +} + +.totalsLabel { + @include type.type-style("label-01"); + color: $text-02; +} + +.totalsValue { + @include type.type-style("heading-04"); + color: $ui-05; +} + +.headerLabelContainer { + display: flex; + align-items: center; + height: spacing.$spacing-07; +} + +.arrowIcon { + fill: var(--cds-link-primary, #0f62fe) !important; +} \ No newline at end of file diff --git a/src/client-registry/client-registry-tiles/client-registry-total-patients-synced-tile.component.tsx b/src/client-registry/client-registry-tiles/client-registry-total-patients-synced-tile.component.tsx new file mode 100644 index 0000000..2e46d24 --- /dev/null +++ b/src/client-registry/client-registry-tiles/client-registry-total-patients-synced-tile.component.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import ClientRegistryTile from "./client-registry-tile/client-registry-tile.component"; + +const TotalPatientsSyncedTileComponent = () => { + const { t } = useTranslation(); + + return ( + + ); +}; + +export default TotalPatientsSyncedTileComponent; diff --git a/src/client-registry/client-registry-tiles/client-registry-total-patients-tile.component.tsx b/src/client-registry/client-registry-tiles/client-registry-total-patients-tile.component.tsx new file mode 100644 index 0000000..84bb0e7 --- /dev/null +++ b/src/client-registry/client-registry-tiles/client-registry-total-patients-tile.component.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import ClientRegistryTile from "./client-registry-tile/client-registry-tile.component"; + +const TotalPatientsTileComponent = () => { + const { t } = useTranslation(); + + return ( + + ); +}; + +export default TotalPatientsTileComponent; diff --git a/src/client-registry/client-registry.component.tsx b/src/client-registry/client-registry.component.tsx new file mode 100644 index 0000000..a18a44e --- /dev/null +++ b/src/client-registry/client-registry.component.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import Header from "../components/header/header.component"; +import Illustration from "./client-registry-illustration.component"; +import ClientRegistrySummaryTiles from "./client-registry-tiles/client-registry-summary-tiles.component"; +import ClientRegistryData from "./client-registry-data/client-registry-data.component"; +import { useTranslation } from "react-i18next"; + +const ClientRegistry: React.FC = () => { + const { t } = useTranslation(); + return ( + <> +
} + title={t("clientRegistry", `Client Registry`)} + /> + + + + ); +}; + +export default ClientRegistry; diff --git a/src/client-registry/client-registry.resource.ts b/src/client-registry/client-registry.resource.ts new file mode 100644 index 0000000..4bb4331 --- /dev/null +++ b/src/client-registry/client-registry.resource.ts @@ -0,0 +1,161 @@ +import { + FetchResponse, + openmrsFetch, + restBaseUrl, +} from "@openmrs/esm-framework"; +import axios from "axios"; +import useSWR from "swr"; + +const v = + "custom:(patientId,uuid,identifiers,display," + + "patientIdentifier:(uuid,identifier)," + + "person:(gender,age,birthdate,birthdateEstimated,personName,addresses,display,dead,deathDate)," + + "attributes:(value,attributeType:(uuid,display)))"; +export interface Payload { + resourceType: string; + identifier: Identifier[]; + name: Name[]; + telecom: Telecom[]; + maritalStatus: MaritalStatus; + managingOrganization: ManagingOrganization; + address: Address[]; + contact: Contact[]; + communication: Communication[]; + extension: Extension[]; + birthDate: string; + deceasedBoolean: boolean; + active: boolean; + gender: string; +} + +export interface Identifier { + system: string; + value: string; + use: string; +} + +export interface Name { + given: string[]; + family: string; + use: string; +} + +export interface Telecom { + value: string; + system: string; + use: string; +} + +export interface MaritalStatus { + coding: Coding[]; + text: string; +} + +export interface Coding { + code: string; + system: string; + display: string; +} + +export interface ManagingOrganization { + identifier: Identifier[]; + telecom: Telecom[]; + active: boolean; + name: string; +} + +export interface Address { + use: string; + district: string; + country: string; +} + +export interface Contact { + relationship: Relationship[]; + name: Name; + address: Address; + telecom: Telecom[]; + gender: string; +} + +export interface Relationship { + text: string; +} + +export interface Communication { + language: Language; + preferred: boolean; +} + +export interface Language { + coding: Coding[]; +} + +export interface Extension { + url: string; + valueReference: ValueReference; +} + +export interface ValueReference { + reference: string; +} + +// get Patients +export function usePatients(q: string, includeDead: boolean) { + const apiUrl = `${restBaseUrl}/patient?q=${q}&v=${v}&includeDead=${includeDead}&totalCount=true&limit=10`; + const { data, error, isLoading, isValidating, mutate } = useSWR< + FetchResponse, + Error + >(apiUrl, openmrsFetch); + + return { + patients: data?.data?.results || [], + isLoading, + isError: error, + isValidating, + mutate, + }; +} + +export async function submitPatient( + apiUrl: string, + payload: Payload +): Promise { + try { + const response = await axios.post(apiUrl, payload, { + headers: { + "Content-Type": "application/json", + }, + }); + return response.data; + } catch (error) { + throw new Error(`Error in submitPatient: ${error.message}`); + } +} + +export function startClientRegistryTask() { + const abortController = new AbortController(); + const apiUrl = `${restBaseUrl}/taskaction`; + const payload = { + action: "runtask", + tasks: ["Client Registry Integration"], + }; + return openmrsFetch(apiUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + signal: abortController.signal, + body: payload, + }); +} + +export function extractErrorMessagesFromResponse(errorObject) { + const fieldErrors = errorObject?.responseBody?.error?.fieldErrors; + if (!fieldErrors) { + return [errorObject?.responseBody?.error?.message ?? errorObject?.message]; + } + return Object.values(fieldErrors).flatMap((errors: Array) => + errors.map((error) => error.message) + ); +} diff --git a/src/client-registry/client-registry.scss b/src/client-registry/client-registry.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/config-schema.ts b/src/config-schema.ts index 11f523b..4878d07 100644 --- a/src/config-schema.ts +++ b/src/config-schema.ts @@ -1 +1,13 @@ -export const configSchema = {}; +import { Type } from "@openmrs/esm-framework"; + +export const configSchema = { + clientRegistryUrl: { + _type: Type.String, + _default: "http://localhost:3000/fhir", + _description: "Concept uuid for the laboratory queue.", + }, +}; + +export type Config = { + clientRegistryUrl: string; +}; diff --git a/src/dashboard.meta.ts b/src/dashboard.meta.ts new file mode 100644 index 0000000..490e080 --- /dev/null +++ b/src/dashboard.meta.ts @@ -0,0 +1,29 @@ +export const registryDashboardMeta = { + title: "Registries", + slotName: "hie-registries-slot", + isExpanded: false, +}; + +export const clientRegistryDashboardMeta = { + slot: "client-registry-dashboard-slot", + columns: 1, + title: "Client", + path: "client-registry-dashboard", + layoutMode: "anchored", +}; + +export const facilityRegistryDashboardMeta = { + slot: "facility-registry-dashboard-slot", + columns: 1, + title: "Facility", + path: "facility-registry-dashboard", + layoutMode: "anchored", +}; + +export const productRegistryDashboardMeta = { + slot: "product-registry-dashboard-slot", + columns: 1, + title: "Product", + path: "product-registry-dashboard", + layoutMode: "anchored", +}; diff --git a/src/facility-registry/facility-registry-illustration.component.tsx b/src/facility-registry/facility-registry-illustration.component.tsx new file mode 100644 index 0000000..30c15d8 --- /dev/null +++ b/src/facility-registry/facility-registry-illustration.component.tsx @@ -0,0 +1,32 @@ +import React from "react"; + +const Illustration: React.FC = () => { + return ( + + + + + + + ); +}; + +export default Illustration; diff --git a/src/facility-registry/facility-registry.component.tsx b/src/facility-registry/facility-registry.component.tsx new file mode 100644 index 0000000..fd8a332 --- /dev/null +++ b/src/facility-registry/facility-registry.component.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import Header from "../components/header/header.component"; +import Illustration from "./facility-registry-illustration.component"; +import { useTranslation } from "react-i18next"; + +const FacilityRegistry: React.FC = () => { + const { t } = useTranslation(); + return ( + <> +
} + title={t("facilityRegistry", `Facility Registry`)} + /> + + ); +}; + +export default FacilityRegistry; diff --git a/src/facility-registry/facility-registry.resource.ts b/src/facility-registry/facility-registry.resource.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/facility-registry/facility-registry.scss b/src/facility-registry/facility-registry.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/index.ts b/src/index.ts index e546cd3..56581a2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,9 +4,22 @@ import { defineConfigSchema, } from "@openmrs/esm-framework"; import { configSchema } from "./config-schema"; -import { createDashboardLink } from "./create-dashboard-link.component"; import { createLeftPanelLink } from "./left-panel-link.component"; import appMenu from "./components/exchange-menu-app/exchange-menu-app-item.component"; +import totalPatientsTileComponent from "./client-registry/client-registry-tiles/client-registry-total-patients-tile.component"; +import totalPatientsSyncedTileComponent from "./client-registry/client-registry-tiles/client-registry-total-patients-synced-tile.component"; +import sendPatientToCRButtonComponent from "./client-registry/client-registry-data/client-registry-actions/send-patient-to-cr-menu-item.component"; +import sendPatientToCRDialogComponent from "./client-registry/client-registry-data/client-registry-dialogs/send-patient-to-cr-dialog.component"; +import clientRegistryDashboardComponent from "./client-registry/client-registry.component"; +import facilityRegistryDashboardComponent from "./facility-registry/facility-registry.component"; +import productRegistryDashboardComponent from "./product-registry/product-registry.component"; +import { createDashboardGroup } from "@openmrs/esm-patient-common-lib"; +import { + clientRegistryDashboardMeta, + facilityRegistryDashboardMeta, + productRegistryDashboardMeta, + registryDashboardMeta, +} from "./dashboard.meta"; const moduleName = "@ugandaemr/esm-ugandaemr-exchange-app"; @@ -24,7 +37,7 @@ export const importTranslation = require.context( export const hieHomeLink = getSyncLifecycle( createLeftPanelLink({ - name: "", + name: "facility-metrics", title: "Facility Metrics", }), options @@ -38,6 +51,14 @@ export const fhirProfileLink = getSyncLifecycle( options ); +export const clientRegistryLink = getSyncLifecycle( + createLeftPanelLink({ + name: "client-registry", + title: "Client Registry", + }), + options +); + export const scheduleManagerLink = getSyncLifecycle( createLeftPanelLink({ name: "schedule-manager", @@ -70,6 +91,25 @@ export const VLSuppressionPredictionWorkspace = getAsyncLifecycle( export const healthExchangeAppMenuItem = getSyncLifecycle(appMenu, options); +export const totalPatientsTile = getSyncLifecycle( + totalPatientsTileComponent, + options +); +export const totalPatientsSyncedTile = getSyncLifecycle( + totalPatientsSyncedTileComponent, + options +); + +export const sendPatientToCRButton = getSyncLifecycle( + sendPatientToCRButtonComponent, + options +); + +export const sendPatientToCRDialog = getSyncLifecycle( + sendPatientToCRDialogComponent, + options +); + export const ChatbotButton = getAsyncLifecycle( () => import("./components/workspace/chatbot/chatbot-button.component"), { @@ -111,3 +151,46 @@ export const pepfarModal = getAsyncLifecycle( import("./facility-metrics/performance/model-components/pepfar.component"), options ); + +export const registriesDashboard = getSyncLifecycle( + createDashboardGroup(registryDashboardMeta), + options +); + +export const clientRegistryDashboardLink = getSyncLifecycle( + createLeftPanelLink({ + ...clientRegistryDashboardMeta, + name: "client-registry-dashboard", + }), + options +); + +export const facilityRegistryDashboardLink = getSyncLifecycle( + createLeftPanelLink({ + ...facilityRegistryDashboardMeta, + name: "facility-registry-dashboard", + }), + options +); + +export const productRegistryDashboardLink = getSyncLifecycle( + createLeftPanelLink({ + ...productRegistryDashboardMeta, + name: "product-registry-dashboard", + }), + options +); + +export const clientRegistry = getSyncLifecycle( + clientRegistryDashboardComponent, + options +); + +export const facilityRegistry = getSyncLifecycle( + facilityRegistryDashboardComponent, + options +); +export const productRegistry = getSyncLifecycle( + productRegistryDashboardComponent, + options +); diff --git a/src/product-registry/product-registry-illustration.component.tsx b/src/product-registry/product-registry-illustration.component.tsx new file mode 100644 index 0000000..30c15d8 --- /dev/null +++ b/src/product-registry/product-registry-illustration.component.tsx @@ -0,0 +1,32 @@ +import React from "react"; + +const Illustration: React.FC = () => { + return ( + + + + + + + ); +}; + +export default Illustration; diff --git a/src/product-registry/product-registry.component.tsx b/src/product-registry/product-registry.component.tsx new file mode 100644 index 0000000..f753069 --- /dev/null +++ b/src/product-registry/product-registry.component.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import Header from "../components/header/header.component"; +import Illustration from "./product-registry-illustration.component"; +import { useTranslation } from "react-i18next"; + +const ProductRegistry: React.FC = () => { + const { t } = useTranslation(); + return ( + <> +
} + title={t("productRegistry", `Product Registry`)} + /> + + ); +}; + +export default ProductRegistry; diff --git a/src/product-registry/product-registry.resource.ts b/src/product-registry/product-registry.resource.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/product-registry/product-registry.scss b/src/product-registry/product-registry.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/root.component.tsx b/src/root.component.tsx index db5d23c..c5523a8 100644 --- a/src/root.component.tsx +++ b/src/root.component.tsx @@ -5,6 +5,9 @@ import LeftPanel from "./components/left-panel/left-panel.component"; import styles from "./root.scss"; import Fhir from "./fhir/fhir.component"; import FacilityMetrics from "./facility-metrics/facility-metrics.component"; +import ClientRegistry from "./client-registry/client-registry.component"; +import FacilityRegistry from "./facility-registry/facility-registry.component"; +import ProductRegistry from "./product-registry/product-registry.component"; import ScheduleManager from "./scheduler/scheduler.component"; const Root: React.FC = () => { @@ -25,6 +28,18 @@ const Root: React.FC = () => { } /> } /> + } + /> + } + /> + } + /> } /> diff --git a/src/routes.json b/src/routes.json index f29fb29..29f8d76 100644 --- a/src/routes.json +++ b/src/routes.json @@ -4,11 +4,14 @@ "fhir2": ">=1.2", "webservices.rest": "^2.24.0" }, - "pages": [{ - "component": "root", - "route": "health-exchange" - }], - "extensions": [{ + "pages": [ + { + "component": "root", + "route": "health-exchange" + } + ], + "extensions": [ + { "component": "root", "name": "health-exchange-dashboard", "slot": "health-exchange-dashboard-slot" @@ -23,6 +26,11 @@ "name": "fhir-profile-link", "slot": "health-exchange-left-panel-slot" }, + { + "component": "registriesDashboard", + "name": "client-registry-link", + "slot": "health-exchange-left-panel-slot" + }, { "name": "vl-suppression-prediction", "component": "VLSuppressionPrediction", @@ -33,13 +41,11 @@ "component": "VLSuppressionPredictionWorkspace" }, { - "name": "chatbot-button", "component": "ChatbotButton", "slot": "homepage-widgets-slot" }, { - "component": "toolsModal", "name": "tools-modal", "online": true, @@ -56,7 +62,33 @@ "name": "pepfar-modal", "online": true, "offline": true - + }, + { + "name": "no-of-patients-tile-component", + "slot": "client-registry-tiles-slot", + "component": "totalPatientsTile", + "meta": { + "name": "PatientsTileSlot", + "title": "Total No of Patients" + } + }, + { + "name": "no-of-patients-synced-tile-component", + "slot": "client-registry-tiles-slot", + "component": "totalPatientsSyncedTile", + "meta": { + "name": "PatientsSyncedTileSlot", + "title": "Total No of Patients Synced" + } + }, + { + "name": "cr-patient-send-to-cr-button", + "component": "sendPatientToCRButton", + "slot": "cr-patients-actions-slot" + }, + { + "name": "send-patient-to-cr-dialog", + "component": "sendPatientToCRDialog" }, { "name": "health-exchange-app-menu-item", @@ -65,7 +97,54 @@ "meta": { "name": "Health Exchange" } - + }, + { + "name": "client-registry-dashboard", + "slot": "hie-registries-slot", + "component": "clientRegistryDashboardLink", + "meta": { + "slot": "client-registry-dashboard-slot", + "columns": 1, + "path": "client-registry-dashboard", + "layoutMode": "anchored" + } + }, + { + "name": "facility-registry-dashboard", + "slot": "hie-registries-slot", + "component": "facilityRegistryDashboardLink", + "meta": { + "slot": "facility-registry-dashboard-slot", + "columns": 1, + "path": "facility-registry-dashboard", + "layoutMode": "anchored" + } + }, + { + "name": "product-registry-dashboard", + "slot": "hie-registries-slot", + "component": "productRegistryDashboardLink", + "meta": { + "slot": "product-registry-dashboard-slot", + "columns": 1, + "path": "product-registry-dashboard", + "layoutMode": "anchored" + } + }, + { + "name": "client-registry-dashboard-ext", + "slot": "client-registry-dashboard-slot", + "component": "clientRegistry" + }, + { + "name": "facility-registry-dashboard-ext", + "slot": "facility-registry-dashboard-slot", + "component": "facilityRegistry" + }, + { + "name": "product-registry-dashboard-ext", + "slot": "product-registry-dashboard-slot", + "component": "productRegistry" }, { "component": "scheduleManagerLink", diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..bf18372 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,59 @@ +export interface Patient { + uuid: string; + display: string; + identifiers: Identifier[]; + person: Person; + voided: boolean; + links: Link[]; + resourceVersion: string; +} + +export interface Identifier { + uuid: string; + display: string; + links: Link[]; +} + +export interface Link { + rel: string; + uri: string; + resourceAlias: string; +} + +export interface Person { + uuid: string; + display: string; + gender: string; + age: number; + birthdate: string; + birthdateEstimated: boolean; + dead: boolean; + deathDate: any; + causeOfDeath: any; + preferredName: PreferredName; + preferredAddress: PreferredAddress; + attributes: Attribute[]; + voided: boolean; + birthtime: any; + deathdateEstimated: boolean; + links: Link[]; + resourceVersion: string; +} + +export interface PreferredName { + uuid: string; + display: string; + links: Link[]; +} + +export interface PreferredAddress { + uuid: string; + display: any; + links: Link[]; +} + +export interface Attribute { + uuid: string; + display: string; + links: Link[]; +} diff --git a/src/ui-components/overflow-menu.component.tsx b/src/ui-components/overflow-menu.component.tsx new file mode 100644 index 0000000..906324f --- /dev/null +++ b/src/ui-components/overflow-menu.component.tsx @@ -0,0 +1,88 @@ +import React, { useState, useCallback, useEffect, useRef } from "react"; +import classNames from "classnames"; +import { useLayoutType } from "@openmrs/esm-framework"; +import styles from "./overflow-menu.scss"; + +interface OrderCustomOverflowMenuComponentProps { + menuTitle: React.ReactNode; + children: React.ReactNode; +} + +const OrderCustomOverflowMenuComponent: React.FC< + OrderCustomOverflowMenuComponentProps +> = ({ children, menuTitle }) => { + const [showMenu, setShowMenu] = useState(false); + const isTablet = useLayoutType() === "tablet"; + const wrapperRef = useRef(null); + + const toggleShowMenu = useCallback(() => setShowMenu((state) => !state), []); + + useEffect(() => { + /** + * Toggle showMenu if clicked on outside of element + */ + function handleClickOutside(event: MouseEvent) { + if (wrapperRef.current && !wrapperRef.current.contains(event.target)) { + setShowMenu(false); + } + } + + // Bind the event listener + document.addEventListener("mousedown", handleClickOutside); + return () => { + // Unbind the event listener on clean up + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [wrapperRef]); + + return ( +
+ + +
+ ); +}; + +export default OrderCustomOverflowMenuComponent; diff --git a/src/ui-components/overflow-menu.scss b/src/ui-components/overflow-menu.scss new file mode 100644 index 0000000..c0d70b9 --- /dev/null +++ b/src/ui-components/overflow-menu.scss @@ -0,0 +1,39 @@ +@use '@carbon/colors'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.container { + position: relative; + top: 0px; +} + +.menu { + display: none; + top: 3.125rem; + min-width: initial; + left: auto; + right: 0; + background-color: $ui-01; + margin-right: 0.2rem; + box-shadow: 0 6px 6px rgb(0 0 0 / 30%); +} + +.show { + display: block; +} + +.overflowMenuButton { + width: auto; + height: auto; + padding: 0.875rem 1rem; + color: colors.$blue-60; + display: flex; + align-items: center; + + &:hover { + background-color: colors.$gray-10-hover; + } +} + +.deceased { + color: colors.$blue-40; +} \ No newline at end of file