-
Notifications
You must be signed in to change notification settings - Fork 103
(feat) Retrieve scheduled appointments in clinical forms workspace #471
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bf00f56
5f84c8b
a9331be
61de4ea
93b8b9d
0098d6e
5d79f3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,13 @@ | ||
| import { fhirBaseUrl, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; | ||
| import { fhirBaseUrl, openmrsFetch, restBaseUrl, toOmrsIsoString } from '@openmrs/esm-framework'; | ||
| import { encounterRepresentation } from '../constants'; | ||
| import type { FHIRObsResource, OpenmrsForm, PatientIdentifier, PatientProgramPayload } from '../types'; | ||
| import type { | ||
| Appointment, | ||
| AppointmentsPayload, | ||
| FHIRObsResource, | ||
| OpenmrsForm, | ||
| PatientIdentifier, | ||
| PatientProgramPayload, | ||
| } from '../types'; | ||
| import { isUuid } from '../utils/boolean-utils'; | ||
|
|
||
| export function saveEncounter(abortController: AbortController, payload, encounterUuid?: string) { | ||
|
|
@@ -18,6 +25,51 @@ export function saveEncounter(abortController: AbortController, payload, encount | |
| }); | ||
| } | ||
|
|
||
| export function addEncounterToAppointments( | ||
| appointments: Array<Appointment>, | ||
| encounterUuid: string, | ||
| abortController: AbortController, | ||
| ) { | ||
| const filteredAppointments = appointments.filter((appointment) => { | ||
| return !appointment.fulfillingEncounters.includes(encounterUuid); | ||
| }); | ||
| return Promise.all( | ||
| filteredAppointments.map((appointment) => updateAppointment(appointment, encounterUuid, abortController)), | ||
| ); | ||
| } | ||
|
|
||
| function updateAppointment( | ||
| appointment: Appointment, | ||
| encounterUuid: string | undefined, | ||
| abortController: AbortController, | ||
| ) { | ||
| const updatedFulfillingEncounters = [...(appointment.fulfillingEncounters ?? []), encounterUuid]; | ||
|
|
||
| const updatedAppointment: AppointmentsPayload = { | ||
| fulfillingEncounters: updatedFulfillingEncounters, | ||
| serviceUuid: appointment.service.uuid, | ||
| locationUuid: appointment.location.uuid, | ||
| patientUuid: appointment.patient.uuid, | ||
| dateAppointmentScheduled: appointment.startDateTime, | ||
| appointmentKind: appointment.appointmentKind, | ||
| status: appointment.status, | ||
| startDateTime: appointment.startDateTime, | ||
| endDateTime: toOmrsIsoString(appointment.endDateTime), | ||
| providers: [{ uuid: appointment.providers[0]?.uuid }], | ||
| comments: appointment.comments, | ||
| uuid: appointment.uuid, | ||
|
Comment on lines
+48
to
+60
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this weird that for updating 1 value of the appointment, we need to pass the whole object? Can this be worked on?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Agree with you @vasharma05. A nice improvement to do in the backend
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jnsereko , can you create a Backend Ticket for this?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree... we should likely have a REST endpoint just for adding a fulfilling encounter to an appointment. |
||
| }; | ||
|
|
||
| return openmrsFetch(`${restBaseUrl}/appointment`, { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| body: updatedAppointment, | ||
| signal: abortController.signal, | ||
| }); | ||
| } | ||
|
|
||
| export function saveAttachment(patientUuid, field, conceptUuid, date, encounterUUID, abortController) { | ||
| const url = `${restBaseUrl}/attachment`; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,26 @@ | ||
| import React from 'react'; | ||
| import React, { useMemo, useState } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
| import { showSnackbar } from '@openmrs/esm-framework'; | ||
| import { formatDatetime, parseDate, showSnackbar } from '@openmrs/esm-framework'; | ||
| import { useLaunchWorkspaceRequiringVisit } from '@openmrs/esm-patient-common-lib'; | ||
| import { Button } from '@carbon/react'; | ||
| import { type FormFieldInputProps } from '../../../types'; | ||
| import { type Appointment, type FormFieldInputProps } from '../../../types'; | ||
| import { isTrue } from '../../../utils/boolean-utils'; | ||
| import styles from './workspace-launcher.scss'; | ||
| import { useFormFactory } from '../../../provider/form-factory-provider'; | ||
| import { DataTable, Table, TableHead, TableRow, TableHeader, TableBody, TableCell } from '@carbon/react'; | ||
| import { InlineNotification } from '@carbon/react'; | ||
|
|
||
| const WorkspaceLauncher: React.FC<FormFieldInputProps> = ({ field }) => { | ||
| const { t } = useTranslation(); | ||
| const { | ||
| patientAppointments: { addAppointmentForCurrentEncounter }, | ||
| } = useFormFactory(); | ||
| const launchWorkspace = useLaunchWorkspaceRequiringVisit(field.questionOptions?.workspaceName); | ||
|
|
||
| const handleAfterCreateAppointment = async (appointmentUuid: string) => { | ||
| addAppointmentForCurrentEncounter(appointmentUuid); | ||
| }; | ||
|
|
||
| const handleLaunchWorkspace = () => { | ||
| if (!launchWorkspace) { | ||
| showSnackbar({ | ||
|
|
@@ -20,7 +30,11 @@ const WorkspaceLauncher: React.FC<FormFieldInputProps> = ({ field }) => { | |
| isLowContrast: true, | ||
| }); | ||
| } | ||
| launchWorkspace(); | ||
| if (field.meta?.handleAppointmentCreation) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Should it really be optional though @vasharma05
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @pirupius , did you have a look here? |
||
| launchWorkspace({ handleAfterCreateAppointment }); | ||
| } else { | ||
| launchWorkspace(); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
|
|
@@ -32,9 +46,85 @@ const WorkspaceLauncher: React.FC<FormFieldInputProps> = ({ field }) => { | |
| {t(field.questionOptions?.buttonLabel) ?? t('launchWorkspace', 'Launch Workspace')} | ||
| </Button> | ||
| </div> | ||
| {field.meta?.handleAppointmentCreation && <AppointmentsTable />} | ||
| </div> | ||
| ) | ||
| ); | ||
| }; | ||
|
|
||
| const AppointmentsTable: React.FC = () => { | ||
| const { t } = useTranslation(); | ||
| const { | ||
| patientAppointments: { appointments, errorFetchingAppointments }, | ||
| } = useFormFactory(); | ||
|
|
||
| const headers = useMemo( | ||
| () => [ | ||
| { key: 'startDateTime', header: t('appointmentDatetime', 'Date & time') }, | ||
| { key: 'location', header: t('location', 'Location') }, | ||
| { key: 'service', header: t('service', 'Service') }, | ||
| { key: 'status', header: t('status', 'Status') }, | ||
| ], | ||
| [t], | ||
| ); | ||
|
|
||
| const rows = useMemo( | ||
| () => | ||
| appointments.map((appointment) => ({ | ||
| id: appointment.uuid, | ||
| startDateTime: formatDatetime(parseDate(appointment.startDateTime), { | ||
| mode: 'standard', | ||
| }), | ||
| location: appointment?.location?.name ? appointment?.location?.name : '——', | ||
| service: appointment.service.name, | ||
| status: appointment.status, | ||
| })), | ||
| [appointments], | ||
| ); | ||
|
|
||
| if (errorFetchingAppointments) { | ||
| return ( | ||
| <InlineNotification | ||
| kind="error" | ||
| title={t('errorFetchingAppointments', 'Error fetching appointments')} | ||
| subtitle={errorFetchingAppointments?.message} | ||
| lowContrast={false} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| if (rows.length === 0) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <DataTable rows={rows} headers={headers}> | ||
| {({ rows, headers, getTableProps, getHeaderProps, getRowProps, getCellProps }) => ( | ||
| <Table {...getTableProps()}> | ||
| <TableHead> | ||
| <TableRow> | ||
| {headers.map((header) => ( | ||
| <TableHeader key={header.key} {...getHeaderProps({ header })}> | ||
| {header.header} | ||
| </TableHeader> | ||
| ))} | ||
| </TableRow> | ||
| </TableHead> | ||
| <TableBody> | ||
| {rows.map((row) => ( | ||
| <TableRow key={row.id} {...getRowProps({ row })}> | ||
| {row.cells.map((cell) => ( | ||
| <TableCell key={cell.id} {...getCellProps({ cell })}> | ||
| {cell.value} | ||
| </TableCell> | ||
| ))} | ||
| </TableRow> | ||
| ))} | ||
| </TableBody> | ||
| </Table> | ||
| )} | ||
| </DataTable> | ||
| ); | ||
| }; | ||
|
|
||
| export default WorkspaceLauncher; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| import { restBaseUrl, useOpenmrsSWR } from '@openmrs/esm-framework'; | ||
| import dayjs from 'dayjs'; | ||
| import type { Appointment, AppointmentsResponse } from '../types'; | ||
| import { useCallback, useMemo, useState } from 'react'; | ||
|
|
||
| export interface UsePatientAppointmentsResults { | ||
| /** | ||
| * All the appointments for the patient and encounter, including the newly created appointments | ||
| */ | ||
| appointments: Array<Appointment>; | ||
| /** | ||
| * The newly created appointments that doesn't have fulfilling encounters yet | ||
| */ | ||
| newlyCreatedAppointments: Array<Appointment>; | ||
| isLoadingAppointments: boolean; | ||
| errorFetchingAppointments: Error; | ||
| isValidatingAppointments: boolean; | ||
| /** | ||
| * When new appointments are created, they need to be added to the list of newly created appointments | ||
| * @param appointmentUuid The UUID of the appointment to add | ||
| */ | ||
| addAppointmentForCurrentEncounter: (appointmentUuid: string) => void; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the appointments for the specified patient and encounter. | ||
| * | ||
| * This hook filters the appointments either the specified encounter UUID, | ||
| * or the newly created appointments that doesn't have fulfilling encounters yet | ||
| * @param patientUuid The UUID of the patient | ||
| * @param encounterUUID The encounter UUID to filter the appointments by their fulfilling encounters | ||
| */ | ||
| export function usePatientAppointments(patientUuid: string, encounterUUID: string): UsePatientAppointmentsResults { | ||
| const [newlyCreatedAppointmentUuids, setNewlyCreatedAppointmentUuids] = useState<Array<string>>([]); | ||
|
|
||
| const startDate = useMemo(() => dayjs().subtract(6, 'month').toISOString(), []); | ||
|
|
||
| // We need to fetch the appointments with the specified fulfilling encounter | ||
| const appointmentsSearchUrl = | ||
| encounterUUID || newlyCreatedAppointmentUuids.length > 0 ? `${restBaseUrl}/appointments/search` : null; | ||
|
|
||
| const { | ||
| data, | ||
| isLoading: isLoadingAppointments, | ||
| error: errorFetchingAppointments, | ||
| isValidating: isValidatingAppointments, | ||
| mutate: refetchAppointments, | ||
| } = useOpenmrsSWR<AppointmentsResponse, Error>(appointmentsSearchUrl, { | ||
| fetchInit: { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| body: { | ||
| patientUuid: patientUuid, | ||
| startDate: startDate, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| const addAppointmentForCurrentEncounter = useCallback( | ||
| (appointmentUuid: string) => { | ||
| setNewlyCreatedAppointmentUuids((prev) => (!prev.includes(appointmentUuid) ? [...prev, appointmentUuid] : prev)); | ||
| refetchAppointments(); | ||
| }, | ||
| [refetchAppointments, setNewlyCreatedAppointmentUuids], | ||
| ); | ||
|
|
||
| const results = useMemo(() => { | ||
| const appointmentsWithEncounter = []; | ||
| const newlyCreatedAppointments = []; | ||
|
|
||
| (data?.data ?? [])?.forEach((appointment) => { | ||
| if (appointment.fulfillingEncounters?.includes(encounterUUID)) { | ||
| appointmentsWithEncounter.push(appointment); | ||
| } else if (newlyCreatedAppointmentUuids.includes(appointment.uuid)) { | ||
| newlyCreatedAppointments.push(appointment); | ||
| } | ||
| }); | ||
|
|
||
| return { | ||
| appointments: [...newlyCreatedAppointments, ...appointmentsWithEncounter], | ||
| newlyCreatedAppointments, | ||
| isLoadingAppointments, | ||
| errorFetchingAppointments, | ||
| isValidatingAppointments, | ||
| addAppointmentForCurrentEncounter, | ||
| }; | ||
| }, [ | ||
| addAppointmentForCurrentEncounter, | ||
| data, | ||
| encounterUUID, | ||
| errorFetchingAppointments, | ||
| isLoadingAppointments, | ||
| isValidatingAppointments, | ||
| newlyCreatedAppointmentUuids, | ||
| ]); | ||
|
|
||
| return results; | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.