diff --git a/packages/esm-patient-clinical-view-app/src/config-schema.ts b/packages/esm-patient-clinical-view-app/src/config-schema.ts index 3096c8b8..f79a7ad4 100644 --- a/packages/esm-patient-clinical-view-app/src/config-schema.ts +++ b/packages/esm-patient-clinical-view-app/src/config-schema.ts @@ -202,6 +202,39 @@ export const configSchema = { }, ], }, + admissionLocationTagUuid: { + _type: Type.UUID, + _description: + 'UUID for the location tag of the `Admission Location`. Patients may only be admitted to inpatient care in a location with this tag', + _default: '233de33e-2778-4f9a-a398-fa09da9daa14', + }, + inpatientVisitUuid: { + _type: Type.UUID, + _description: 'UUID for the inpatient visit', + _default: 'a73e2ac6-263b-47fc-99fc-e0f2c09fc914', + }, + inPatientForms: { + _type: Type.Array, + _description: 'List of forms that can be filled out for in-patients', + _default: [ + { + label: 'Cardex Nursing Plan', + uuid: '89891ea0-444f-48bf-98e6-f97e87607f7e', + }, + { + label: 'IPD Procedure Form', + uuid: '3853ed6d-dddd-4459-b441-25cd6a459ed4', + }, + { + label: 'Newborn Unit Admission ', + uuid: 'e8110437-e3cc-4238-bfc1-414bdd4de6a4', + }, + { + label: 'Partograph Form', + uuid: '3791e5b7-2cdc-44fc-982b-a81135367c96', + }, + ], + }, }; export interface ConfigObject { @@ -236,6 +269,48 @@ export interface ConfigObject { }; familyRelationshipsTypeList: Array<{ uuid: string; display: string }>; pnsRelationships: Array<{ uuid: string; display: string; sexual: boolean }>; + admissionLocationTagUuid: { + _type: Type.UUID; + _description: 'UUID for the location tag of the `Admission Location`. Patients may only be admitted to inpatient care in a location with this tag'; + _default: '233de33e-2778-4f9a-a398-fa09da9daa14'; + }; + inpatientVisitUuid: { + _type: Type.UUID; + _description: 'UUID for the inpatient visit'; + _default: 'a73e2ac6-263b-47fc-99fc-e0f2c09fc914'; + }; + restrictWardAdministrationToLoginLocation: { + _type: Type.Boolean; + _description: 'UUID for the inpatient visit'; + _default: false; + }; + patientListForAdmissionUrl: { + _type: Type.String; + _description: 'Endpoint for fetching list of patients eligible for ward admission'; + _default: ''; + }; + inPatientForms: { + _type: Type.Array; + _description: 'List of forms that can be filled out for in-patients'; + _default: [ + { + label: 'Cardex Nursing Plan'; + uuid: '89891ea0-444f-48bf-98e6-f97e87607f7e'; + }, + { + label: 'IPD Procedure Form'; + uuid: '3853ed6d-dddd-4459-b441-25cd6a459ed4'; + }, + { + label: 'Newborn Unit Admission '; + uuid: 'e8110437-e3cc-4238-bfc1-414bdd4de6a4'; + }, + { + label: 'Partograph Form'; + uuid: '3791e5b7-2cdc-44fc-982b-a81135367c96'; + }, + ]; + }; } export interface PartograpyComponents { @@ -254,3 +329,11 @@ export interface ConfigPartographyObject { descentOfHead: string; }; } + +export type BedManagementConfig = { + admissionLocationTagUuid: string; + inpatientVisitUuid: string; + restrictWardAdministrationToLoginLocation: boolean; + patientListForAdmissionUrl: string; + inPatientForms: Array<{ label: string; uuid: string }>; +}; diff --git a/packages/esm-patient-clinical-view-app/src/in-patient/encounter-observations/encounter-observations.component.tsx b/packages/esm-patient-clinical-view-app/src/in-patient/encounter-observations/encounter-observations.component.tsx new file mode 100644 index 00000000..e941af18 --- /dev/null +++ b/packages/esm-patient-clinical-view-app/src/in-patient/encounter-observations/encounter-observations.component.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { SkeletonText } from '@carbon/react'; +import { useConfig } from '@openmrs/esm-framework'; +import { type Observation } from './visit.resource'; +import styles from './styles.scss'; + +interface EncounterObservationsProps { + observations: Array; +} + +const EncounterObservations: React.FC = ({ observations }) => { + const { t } = useTranslation(); + const { obsConceptUuidsToHide = [] } = useConfig(); + + function getAnswerFromDisplay(display: string): string { + const colonIndex = display.indexOf(':'); + if (colonIndex === -1) { + return ''; + } else { + return display.substring(colonIndex + 1).trim(); + } + } + + if (!observations) { + return ; + } + + if (observations) { + const filteredObservations = !!obsConceptUuidsToHide.length + ? observations?.filter((obs) => { + return !obsConceptUuidsToHide.includes(obs?.concept?.uuid); + }) + : observations; + return ( +
+ {filteredObservations?.map((obs, index) => { + if (obs.groupMembers) { + return ( + + {obs.concept.display} + + {obs.groupMembers.map((member) => ( + + {member.concept.display} + {getAnswerFromDisplay(member.display)} + + ))} + + ); + } else { + return ( + + {obs.concept.display} + {getAnswerFromDisplay(obs.display)} + + ); + } + })} +
+ ); + } + + return ( +
+

{t('noObservationsFound', 'No observations found')}

+
+ ); +}; + +export default EncounterObservations; diff --git a/packages/esm-patient-clinical-view-app/src/in-patient/encounter-observations/index.ts b/packages/esm-patient-clinical-view-app/src/in-patient/encounter-observations/index.ts new file mode 100644 index 00000000..9320acd8 --- /dev/null +++ b/packages/esm-patient-clinical-view-app/src/in-patient/encounter-observations/index.ts @@ -0,0 +1,3 @@ +import EncounterObservations from './encounter-observations.component'; + +export default EncounterObservations; diff --git a/packages/esm-patient-clinical-view-app/src/in-patient/encounter-observations/styles.scss b/packages/esm-patient-clinical-view-app/src/in-patient/encounter-observations/styles.scss new file mode 100644 index 00000000..c53892ed --- /dev/null +++ b/packages/esm-patient-clinical-view-app/src/in-patient/encounter-observations/styles.scss @@ -0,0 +1,22 @@ +@use '@carbon/styles/scss/spacing'; + +.observation { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: spacing.$spacing-03; + margin-block: spacing.$spacing-05; + margin-inline: 0 spacing.$spacing-05; +} + +.observation > span { + align-self: center; + justify-self: start; +} + +.parentConcept { + font-weight: bold; +} + +.childConcept { + padding-inline-start: 0.8rem; +} diff --git a/packages/esm-patient-clinical-view-app/src/in-patient/encounter-observations/visit.resource.tsx b/packages/esm-patient-clinical-view-app/src/in-patient/encounter-observations/visit.resource.tsx new file mode 100644 index 00000000..8632c93d --- /dev/null +++ b/packages/esm-patient-clinical-view-app/src/in-patient/encounter-observations/visit.resource.tsx @@ -0,0 +1,161 @@ +import { OpenmrsResource, Privilege } from '@openmrs/esm-framework'; + +export interface MappedEncounter { + id: string; + datetime: string; + encounterType: string; + editPrivilege: string; + form: OpenmrsResource; + obs: Array; + provider: string; + visitUuid: string; + visitType: string; + visitTypeUuid?: string; + visitStartDatetime?: string; + visitStopDatetime?: string; +} + +export interface Encounter { + uuid: string; + diagnoses: Array; + encounterDatetime: string; + encounterProviders: Array; + encounterType: { + uuid: string; + display: string; + viewPrivilege: Privilege; + editPrivilege: Privilege; + }; + obs: Array; + orders: Array; + form: OpenmrsResource; + patient: OpenmrsResource; +} + +export interface EncounterProvider { + uuid: string; + display: string; + encounterRole: { + uuid: string; + display: string; + }; + provider: { + uuid: string; + person: { + uuid: string; + display: string; + }; + }; +} + +export interface Observation { + uuid: string; + concept: { + uuid: string; + display: string; + conceptClass: { + uuid: string; + display: string; + }; + }; + display: string; + groupMembers: null | Array<{ + uuid: string; + concept: { + uuid: string; + display: string; + }; + value: { + uuid: string; + display: string; + }; + display: string; + }>; + value: any; + obsDatetime?: string; +} + +export interface Order { + uuid: string; + dateActivated: string; + dateStopped?: Date | null; + dose: number; + dosingInstructions: string | null; + dosingType?: 'org.openmrs.FreeTextDosingInstructions' | 'org.openmrs.SimpleDosingInstructions'; + doseUnits: { + uuid: string; + display: string; + }; + drug: { + uuid: string; + name: string; + strength: string; + display: string; + }; + duration: number; + durationUnits: { + uuid: string; + display: string; + }; + frequency: { + uuid: string; + display: string; + }; + numRefills: number; + orderNumber: string; + orderReason: string | null; + orderReasonNonCoded: string | null; + orderer: { + uuid: string; + person: { + uuid: string; + display: string; + }; + }; + orderType: { + uuid: string; + display: string; + }; + route: { + uuid: string; + display: string; + }; + quantity: number; + quantityUnits: OpenmrsResource; +} + +export interface Note { + concept: OpenmrsResource; + note: string; + provider: { + name: string; + role: string; + }; + time: string; +} + +export interface OrderItem { + order: Order; + provider: { + name: string; + role: string; + }; +} + +export interface Diagnosis { + certainty: string; + display: string; + encounter: OpenmrsResource; + links: Array; + patient: OpenmrsResource; + rank: number; + resourceVersion: string; + uuid: string; + voided: boolean; + diagnosis: { + coded: { + display: string; + links: Array; + }; + }; +} diff --git a/packages/esm-patient-clinical-view-app/src/in-patient/in-patient-table/in-patient-table.component.tsx b/packages/esm-patient-clinical-view-app/src/in-patient/in-patient-table/in-patient-table.component.tsx new file mode 100644 index 00000000..f33db1c1 --- /dev/null +++ b/packages/esm-patient-clinical-view-app/src/in-patient/in-patient-table/in-patient-table.component.tsx @@ -0,0 +1,155 @@ +import React, { useMemo } from 'react'; +import { + DataTable, + TableContainer, + Table, + TableHead, + TableRow, + TableExpandHeader, + TableHeader, + TableBody, + TableExpandRow, + TableExpandedRow, + TableCell, + Button, +} from '@carbon/react'; +import { Edit } from '@carbon/react/icons'; +import { Encounter } from '../encounter-observations/visit.resource'; +import { useTranslation } from 'react-i18next'; +import { formatDatetime, usePagination, Visit } from '@openmrs/esm-framework'; +import EncounterObservations from '../encounter-observations'; +import { EmptyState, launchPatientWorkspace, PatientChartPagination } from '@openmrs/esm-patient-common-lib'; +import styles from './in-patient-table.scss'; +import { mutate } from 'swr'; + +type InPatientTableProps = { + tableRows: Array; + currentVisit: Visit; +}; + +const InPatientTable: React.FC = ({ tableRows }) => { + const { t } = useTranslation(); + const headers = [ + { key: 'dateTime', header: t('dateDate', 'Date & time') }, + { key: 'formName', header: t('formName', 'Form Name') }, + { key: 'provider', header: t('provider', 'Provider') }, + { key: 'encounterType', header: t('encounterType', 'Encounter Type') }, + ]; + + const { results, goTo, currentPage } = usePagination(tableRows, 10); + + const paginatedRows = useMemo(() => { + return results.map((row) => ({ + id: row.uuid, + dateTime: formatDatetime(new Date(row.encounterDatetime), { + mode: 'standard', + }), + formName: row.form.display, + provider: row.encounterProviders[0]?.provider?.person?.display, + encounterType: row.encounterType.display, + })); + }, [results]); + + const onEncounterEdit = (encounter: Encounter) => { + launchPatientWorkspace('patient-form-entry-workspace', { + workspaceTitle: encounter.form.display, + mutateForm: () => { + mutate((key) => typeof key === 'string' && key.startsWith(`/ws/rest/v1/encounter`), undefined, { + revalidate: true, + }); + }, + formInfo: { + encounterUuid: encounter.uuid, + formUuid: encounter?.form?.uuid, + additionalProps: {}, + }, + }); + }; + + if (results.length === 0) { + return ( + + ); + } + + return ( + <> + + {({ + rows, + headers, + getHeaderProps, + getRowProps, + + getTableProps, + getTableContainerProps, + }) => ( + + + + + + {headers.map((header, i) => ( + + {header.header} + + ))} + + + + {rows.map((row, index) => ( + + + {row.cells.map((cell) => ( + {cell.value} + ))} + + {row.isExpanded ? ( + + <> + + + <> + {results[index]?.form?.uuid && ( + + )} + + + + ) : ( + + )} + + ))} + +
+
+ )} +
+ goTo(page)} + /> + + ); +}; + +export default InPatientTable; diff --git a/packages/esm-patient-clinical-view-app/src/in-patient/in-patient-table/in-patient-table.scss b/packages/esm-patient-clinical-view-app/src/in-patient/in-patient-table/in-patient-table.scss new file mode 100644 index 00000000..21d1ef26 --- /dev/null +++ b/packages/esm-patient-clinical-view-app/src/in-patient/in-patient-table/in-patient-table.scss @@ -0,0 +1,37 @@ +@use '@carbon/styles/scss/spacing'; +@use '@carbon/styles/scss/type'; +@use '@openmrs/esm-styleguide/src/vars' as *; + +.tableContainer { + padding: 0; + + :global(.cds--data-table-header) { + padding: 0; + } + + :global(.cds--table-toolbar) { + position: relative; + overflow: visible; + top: 0; + } + + &:global(.cds--data-table-container) { + background: none !important; + } +} + +.expandedRow { + padding-inline-start: 3.5rem; + + >td { + padding: inherit !important; + + >div { + max-height: max-content !important; + } + } + + >div { + background-color: $ui-02; + } +} \ No newline at end of file diff --git a/packages/esm-patient-clinical-view-app/src/in-patient/in-patient.component.tsx b/packages/esm-patient-clinical-view-app/src/in-patient/in-patient.component.tsx new file mode 100644 index 00000000..c7d5d6c0 --- /dev/null +++ b/packages/esm-patient-clinical-view-app/src/in-patient/in-patient.component.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { ComboButton, MenuItem, DataTableSkeleton } from '@carbon/react'; +import { CardHeader, EmptyState, launchPatientWorkspace } from '@openmrs/esm-patient-common-lib'; +import { useConfig, useVisit } from '@openmrs/esm-framework'; +import { BedManagementConfig } from '../config-schema'; +import { usePatientEncounters } from './in-patient.resource'; +import InPatientTable from './in-patient-table/in-patient-table.component'; + +type InPatientProps = { + patientUuid: string; +}; + +const InPatient: React.FC = ({ patientUuid }) => { + const { t } = useTranslation(); + const { inpatientVisitUuid } = useConfig(); + const { currentVisit } = useVisit(patientUuid); + const { inPatientForms } = useConfig(); + const { encounters, isLoading, mutate } = usePatientEncounters(patientUuid); + + const hasInPatientVisit = currentVisit?.visitType.uuid === inpatientVisitUuid; + + const handleLaunchForm = (form: { label: string; uuid: string }) => { + launchPatientWorkspace('patient-form-entry-workspace', { + workspaceTitle: form.label, + mutateForm: () => mutate(), + formInfo: { + encounterUuid: '', + formUuid: form.uuid, + additionalProps: {}, + }, + }); + }; + + if (!hasInPatientVisit) { + return ( + + ); + } + + return ( +
+ + + {inPatientForms.map((form) => ( + handleLaunchForm(form)} label={form.label} /> + ))} + + +
+ {isLoading && } + {!isLoading && } +
+
+ ); +}; + +export default InPatient; diff --git a/packages/esm-patient-clinical-view-app/src/in-patient/in-patient.meta.tsx b/packages/esm-patient-clinical-view-app/src/in-patient/in-patient.meta.tsx new file mode 100644 index 00000000..4b6bdb1a --- /dev/null +++ b/packages/esm-patient-clinical-view-app/src/in-patient/in-patient.meta.tsx @@ -0,0 +1,9 @@ +export const inPatientMeta = { + slot: 'patient-chart-in-patient-dashboard-slot', + path: 'in-patient', + title: 'In Patient', + moduleName: '@kenyaemr/esm-bed-management-app', + name: 'In Patient', + columns: 1, + config: {}, +}; diff --git a/packages/esm-patient-clinical-view-app/src/in-patient/in-patient.resource.tsx b/packages/esm-patient-clinical-view-app/src/in-patient/in-patient.resource.tsx new file mode 100644 index 00000000..1cfc948b --- /dev/null +++ b/packages/esm-patient-clinical-view-app/src/in-patient/in-patient.resource.tsx @@ -0,0 +1,26 @@ +import { openmrsFetch, useConfig } from '@openmrs/esm-framework'; +import useSWR from 'swr'; +import { Encounter } from './encounter-observations/visit.resource'; +import { BedManagementConfig } from '../config-schema'; + +export const usePatientEncounters = (patientUuid: string) => { + const { inPatientForms } = useConfig(); + const { data, isLoading, error, mutate } = useSWR<{ + data: { results: Array }; + }>( + `/ws/rest/v1/encounter?patient=${patientUuid}&v=custom:(uuid,display,encounterDatetime,obs:full,form:(uuid,display),encounterType:(uuid,display),encounterProviders:(uuid,display,encounterRole:(uuid,display),provider:(uuid,person:(uuid,display))),orders:(uuid,display),diagnoses:(uuid,display)`, + openmrsFetch, + ); + + const encounters = + data?.data?.['results']?.filter((encounter) => + inPatientForms?.find((form) => form.uuid === encounter?.form?.uuid), + ) ?? []; + + return { + encounters: encounters, + isLoading, + error, + mutate, + }; +}; diff --git a/packages/esm-patient-clinical-view-app/src/index.ts b/packages/esm-patient-clinical-view-app/src/index.ts index d60944c4..0e40e548 100644 --- a/packages/esm-patient-clinical-view-app/src/index.ts +++ b/packages/esm-patient-clinical-view-app/src/index.ts @@ -44,6 +44,8 @@ import CaseEncounterOverviewComponent from './case-management/encounters/case-en import FamilyRelationshipForm from './family-partner-history/family-relationship.workspace'; import { OtherRelationships } from './other-relationships/other-relationships.component'; import { OtherRelationshipsForm } from './other-relationships/other-relationships.workspace'; +import InPatient from './in-patient/in-patient.component'; +import { inPatientMeta } from './in-patient/in-patient.meta'; const moduleName = '@kenyaemr/esm-patient-clinical-view-app'; @@ -126,6 +128,9 @@ export const labourAndDeliveryLink = getSyncLifecycle(createDashboardLink(labour export const genericNavLinks = getSyncLifecycle(GenericNavLinks, options); export const genericDashboard = getSyncLifecycle(GenericDashboard, options); +export const inPatientChartLink = getSyncLifecycle(createDashboardLink(inPatientMeta), options); +export const inPatientChartDashboard = getSyncLifecycle(InPatient, options); + export function startupApp() { defineConfigSchema(moduleName, configSchema); } diff --git a/packages/esm-patient-clinical-view-app/src/routes.json b/packages/esm-patient-clinical-view-app/src/routes.json index 24725f1d..4c217891 100644 --- a/packages/esm-patient-clinical-view-app/src/routes.json +++ b/packages/esm-patient-clinical-view-app/src/routes.json @@ -206,6 +206,27 @@ "path": "/case-management" } }, + { + "name": "in-patient-dashboard-link", + "component": "inPatientChartLink", + "slot": "patient-chart-dashboard-slot", + "order": 7, + "meta": { + "slot": "patient-chart-in-patient-dashboard-slot", + "path": "in-patient", + "layoutMode": "anchored", + "columns": 1, + "columnSpan": 1 + } + }, + { + "name": "in-patient-dashboard", + "slot": "patient-chart-in-patient-dashboard-slot", + "component": "inPatientChartDashboard", + "meta": { + "fullWidth": false + } + }, { "name": "wrap-component-view", "slot": "case-management-dashboard-slot", diff --git a/packages/esm-patient-clinical-view-app/src/summary/summary.component.tsx b/packages/esm-patient-clinical-view-app/src/summary/summary.component.tsx new file mode 100644 index 00000000..d2564ae3 --- /dev/null +++ b/packages/esm-patient-clinical-view-app/src/summary/summary.component.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { DataTableSkeleton } from '@carbon/react'; +import { ArrowRight } from '@carbon/react/icons'; +import { useTranslation } from 'react-i18next'; +import { ConfigurableLink } from '@openmrs/esm-framework'; +import { useAdmissionLocations } from './summary.resource'; +import WardCard from '../ward-card/ward-card.component'; +import styles from './summary.scss'; +import { EmptyState, ErrorState } from '@openmrs/esm-patient-common-lib'; + +const Summary: React.FC = () => { + const { t } = useTranslation(); + const { data: admissionLocations, isLoading, error } = useAdmissionLocations(); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (admissionLocations?.length) { + return ( +
+ {admissionLocations.map((admissionLocation) => { + const routeSegment = `${window.getOpenmrsSpaBase()}bed-management/location/${admissionLocation.ward.uuid}`; + + return ( + + {admissionLocation?.totalBeds && ( +
+ + {t('viewBeds', 'View beds')} + + +
+ )} +
+ ); + })} +
+ ); + } + + if (!isLoading && admissionLocations?.length === 0 && !error) { + return ; + } + + if (error) { + return ( + + ); + } +}; + +export default Summary; diff --git a/packages/esm-patient-clinical-view-app/src/summary/summary.resource.ts b/packages/esm-patient-clinical-view-app/src/summary/summary.resource.ts new file mode 100644 index 00000000..0d5fbd6d --- /dev/null +++ b/packages/esm-patient-clinical-view-app/src/summary/summary.resource.ts @@ -0,0 +1,329 @@ +import useSWR from 'swr'; +import { FetchResponse, openmrsFetch, showToast } from '@openmrs/esm-framework'; +import { Bed, AdmissionLocation, MappedBedData } from '../types'; + +export const useLocationsByTag = (locationUuid: string) => { + const locationsUrl = `/ws/rest/v1/location?tag=Admission&v=full`; + + const { data, error, isLoading, isValidating, mutate } = useSWR<{ data }, Error>( + locationUuid ? locationsUrl : null, + openmrsFetch, + ); + + return { + data: data?.data?.results ?? [], + error, + isLoading, + isValidating, + mutate, + }; +}; + +export const getBedsForLocation = (locationUuid: string) => { + const locationsUrl = `/ws/rest/v1/bed?locationUuid=${locationUuid}`; + + return openmrsFetch(locationsUrl, { + method: 'GET', + }).then((res) => res?.data?.results ?? []); +}; + +export const useBedsForLocation = (locationUuid: string) => { + const apiUrl = `/ws/rest/v1/bed?locationUuid=${locationUuid}&v=full`; + + const { data, isLoading, error } = useSWR<{ data: { results: Array } }, Error>( + locationUuid ? apiUrl : null, + openmrsFetch, + ); + + const mappedBedData: MappedBedData = (data?.data?.results ?? []).map((bed) => ({ + id: bed.id, + number: bed.bedNumber, + name: bed.bedType?.displayName, + description: bed.bedType?.description, + status: bed.status, + uuid: bed.uuid, + })); + + return { + bedData: mappedBedData, + isLoading, + error, + }; +}; + +export const useLocationName = (locationUuid: string) => { + const apiUrl = `/ws/rest/v1/location/${locationUuid}`; + + const { data, isLoading } = useSWR<{ data }, Error>(locationUuid ? apiUrl : null, openmrsFetch); + + return { + name: data?.data?.display ?? null, + isLoadingLocationData: isLoading, + }; +}; + +export const findBedByLocation = (locationUuid: string) => { + const locationsUrl = `/ws/rest/v1/bed?locationUuid=${locationUuid}`; + return openmrsFetch(locationsUrl, { + method: 'GET', + }); +}; + +export const useWards = (locationUuid: string) => { + const locationsUrl = `/ws/rest/v1/location?tag=${locationUuid}&v=full`; + const { data, error, isLoading, isValidating, mutate } = useSWR<{ data }, Error>( + locationUuid ? locationsUrl : null, + openmrsFetch, + ); + + return { + data, + error, + isLoading, + isValidating, + mutate, + }; +}; + +export const useAdmissionLocations = () => { + const locationsUrl = `/ws/rest/v1/admissionLocation?v=full`; + const { data, error, isLoading, isValidating, mutate } = useSWR< + { data: { results: Array } }, + Error + >(locationsUrl, openmrsFetch); + + return { + data: data?.data?.results ?? [], + error, + isLoading, + isValidating, + mutate, + }; +}; + +export const useAdmissionLocationBedLayout = (locationUuid: string) => { + const locationsUrl = `/ws/rest/v1/admissionLocation/${locationUuid}?v=full`; + const { data, error, isLoading, isValidating, mutate } = useSWR<{ data: AdmissionLocation }, Error>( + locationsUrl, + openmrsFetch, + ); + + return { + data: data?.data?.bedLayouts ?? [], + error, + isLoading, + isValidating, + mutate, + }; +}; +export const useBedType = () => { + const url = `/ws/rest/v1/bedtype/`; + const { data, error, isLoading, isValidating, mutate } = useSWR<{ data }, Error>(url, openmrsFetch); + const results = data?.data?.results ? data?.data?.results : []; + return { + bedTypeData: results, + isError: error, + loading: isLoading, + validate: isValidating, + mutate, + }; +}; + +export const useBedTag = () => { + const url = `/ws/rest/v1/bedTag/`; + const { data, error, isLoading, isValidating, mutate } = useSWR<{ data }, Error>(url, openmrsFetch); + const results = data?.data?.results ? data?.data?.results : []; + return { + bedTypeData: results, + isError: error, + loading: isLoading, + validate: isValidating, + mutate, + }; +}; +interface BedType { + name: string; + displayName: string; + description: string; +} +interface BedTag { + name: string; +} +export async function saveBedType({ bedPayload }): Promise> { + try { + const response: FetchResponse = await openmrsFetch(`/ws/rest/v1/bedtype`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: bedPayload, + }); + return response; + } catch (error) { + const errorMessages = extractErrorMessagesFromResponse(error); + showToast({ + description: errorMessages.join(', '), + title: 'Error on saving form', + kind: 'error', + critical: true, + }); + } +} + +interface WardName { + name: string; +} +export async function saveBedTag({ bedPayload }): Promise> { + try { + const response: FetchResponse = await openmrsFetch(`/ws/rest/v1/bedTag/`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: bedPayload, + }); + return response; + } catch (error) { + const errorMessages = extractErrorMessagesFromResponse(error); + showToast({ + description: errorMessages.join(', '), + title: 'Error on saving form', + kind: 'error', + critical: true, + }); + } +} +export async function editBedType({ bedPayload, bedTypeId }): Promise> { + try { + const response: FetchResponse = await openmrsFetch(`/ws/rest/v1/bedtype/${bedTypeId}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: bedPayload, + }); + return response; + } catch (error) { + const errorMessages = extractErrorMessagesFromResponse(error); + showToast({ + description: errorMessages.join(', '), + title: 'Error on saving form', + kind: 'error', + critical: true, + }); + } +} +export async function editBedTag({ bedPayload, bedTagId }): Promise> { + try { + const response: FetchResponse = await openmrsFetch(`/ws/rest/v1/bedTag/${bedTagId}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: bedPayload, + }); + return response; + } catch (error) { + const errorMessages = extractErrorMessagesFromResponse(error); + showToast({ + description: errorMessages.join(', '), + title: 'Error on saving form', + kind: 'error', + critical: true, + }); + } +} + +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)); +} + +export async function deleteBedTag(bedTagId: string): Promise> { + try { + const response: FetchResponse = await openmrsFetch(`/ws/rest/v1/bedTag/${bedTagId}`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + }); + + if (response.ok) { + return response; + } else { + throw new Error(`Failed to delete bed tag. Status: ${response.status}`); + } + } catch (error) { + const errorMessages = extractErrorMessagesFromResponse(error); + showToast({ + description: errorMessages.join(', '), + title: 'Error on deleting bed tag', + kind: 'error', + critical: true, + }); + } +} + +export async function deleteBedType(bedtype: string): Promise> { + try { + const response: FetchResponse = await openmrsFetch(`/ws/rest/v1/bedtype/${bedtype}`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + }); + + if (response.ok) { + return response; + } else { + throw new Error(`Failed to delete bed tag. Status: ${response.status}`); + } + } catch (error) { + const errorMessages = extractErrorMessagesFromResponse(error); + showToast({ + description: errorMessages.join(', '), + title: 'Error on saving form', + kind: 'error', + critical: true, + }); + } +} + +export const useWard = () => { + const url = `ws/rest/v1/location/`; + const { data, error, isLoading, isValidating, mutate } = useSWR<{ data }, Error>(url, openmrsFetch); + const results = data?.data?.results ? data?.data?.results : []; + + return { + wardList: results, + isError: error, + loading: isLoading, + validate: isValidating, + mutate, + }; +}; +export const useLocationTags = () => { + const url = `ws/rest/v1/locationtag`; + const { data, error, isLoading, isValidating, mutate } = useSWR<{ data }, Error>(url, openmrsFetch); + const results = data?.data?.results ? data?.data?.results : []; + + return { + tagList: results, + tagError: error, + tagLoading: isLoading, + tagValidate: isValidating, + tagMutate: mutate, + }; +}; + +export async function saveWard({ wardPayload }): Promise> { + try { + const response: FetchResponse = await openmrsFetch(`/ws/rest/v1/location/`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: wardPayload, + }); + return response; + } catch (error) { + const errorMessages = extractErrorMessagesFromResponse(error); + showToast({ + description: errorMessages.join(', '), + title: 'Error on saving form', + kind: 'error', + critical: true, + }); + } +} diff --git a/packages/esm-patient-clinical-view-app/src/summary/summary.scss b/packages/esm-patient-clinical-view-app/src/summary/summary.scss new file mode 100644 index 00000000..39c55b32 --- /dev/null +++ b/packages/esm-patient-clinical-view-app/src/summary/summary.scss @@ -0,0 +1,72 @@ +@use '@carbon/colors'; +@use '@carbon/layout'; + +.cardContainer { + background-color: colors.$white-0; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + justify-content: space-between; + padding: 1rem; +} + +.buttonMain { + background-color: colors.$white-0; + color: colors.$teal-60; + cursor: text; +} + +.sectionLoader { + text-align: center; +} + +.buttonMain { + &:active, &:focus, &:hover { + color: colors.$teal-60 !important; + background-color: colors.$white-0 !important; + } +} + +.inactiveMain { + margin-bottom: 4rem; +} + +.buttonContainer { + display: flex; +} + +.buttonItems { + flex: auto; +} + +.pageHeaderContainer { + display: flex; + justify-content: space-between; + align-items: center; + padding: layout.$spacing-05; + background-color: #ededed; +} + +.pageHeader { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + font-size: 1.25rem; + font-weight: 400; + line-height: 1.4; + letter-spacing: 0; +} + +.link { + text-decoration: none; + display: flex; + align-items: center; + color: colors.$blue-60; + + svg { + margin-left: layout.$spacing-03; + } +} + +.loader { + margin: 2rem; +} diff --git a/packages/esm-patient-clinical-view-app/src/types/index.ts b/packages/esm-patient-clinical-view-app/src/types/index.ts index 223efaa3..f6ecf7d1 100644 --- a/packages/esm-patient-clinical-view-app/src/types/index.ts +++ b/packages/esm-patient-clinical-view-app/src/types/index.ts @@ -148,3 +148,57 @@ export interface HTSEncounter { }; }[]; } + +export interface BedDetails extends Bed { + patient: null | { + uuid: string; + person: { + gender: string; + age: number; + preferredName: { + givenName: string; + familyName: string; + }; + }; + identifiers: Array<{ identifier: string }>; + }; +} + +export type AdmissionLocation = { + ward: { + uuid: string; + display: string; + name: string; + description: string; + }; + totalBeds: number; + occupiedBeds: number; + bedLayouts: Array; +}; + +export interface Bed { + id: number; + bedId: number; + uuid: string; + bedNumber: string; + bedType: { + uuid: string; + name: string; + displayName: string; + description: string; + resourceVersion: string; + }; + row: number; + column: number; + status: 'AVAILABLE' | string; + location: string; +} + +export type MappedBedData = Array<{ + id: number; + number: string; + name: string; + description: string; + status: string; + uuid: string; +}>; diff --git a/packages/esm-patient-clinical-view-app/src/ward-card/ward-card.component.tsx b/packages/esm-patient-clinical-view-app/src/ward-card/ward-card.component.tsx new file mode 100644 index 00000000..8f9053ba --- /dev/null +++ b/packages/esm-patient-clinical-view-app/src/ward-card/ward-card.component.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Layer, Tile } from '@carbon/react'; +import styles from './ward-card.scss'; + +interface WardCardProps { + label: string; + value: number | string; + headerLabel: string; + children?: React.ReactNode; + service?: string; + serviceUuid?: string; + locationUuid?: string; +} + +const WardCard: React.FC = ({ children, headerLabel, label, value }) => { + return ( + + +
+
+ +
+ {children} +
+
+ +

{value}

+
+
+
+ ); +}; + +export default WardCard; diff --git a/packages/esm-patient-clinical-view-app/src/ward-card/ward-card.scss b/packages/esm-patient-clinical-view-app/src/ward-card/ward-card.scss new file mode 100644 index 00000000..79fe9fea --- /dev/null +++ b/packages/esm-patient-clinical-view-app/src/ward-card/ward-card.scss @@ -0,0 +1,51 @@ +@use '@carbon/colors'; +@use '@carbon/layout'; +@use '@carbon/type'; + +.container { + flex-grow: 1; +} + +.tileContainer { + border: 1px solid colors.$gray-20; + height: 7.875rem; + padding: layout.$spacing-05; + margin: layout.$spacing-03 layout.$spacing-03; +} + +.tileHeader { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: layout.$spacing-03; +} + +.headerLabel { + @include type.type-style('heading-compact-01'); + color: colors.$gray-70; +} + +.totalsLabel { + @include type.type-style('label-01'); + color: colors.$gray-70; +} + +.totalsValue { + @include type.type-style('heading-04'); + color: colors.$gray-100; +} + +.headerLabelContainer { + margin-bottom: 1rem; +} + +.link { + text-decoration: none; + display: flex; + align-items: center; + color: colors.$blue-60; + + svg { + margin-left: layout.$spacing-03; + } +}