Skip to content
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

Scheduling patient #241

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
resourceType: Mapping
id: new-appointment-proposed-patient-extract
body:
$let:
predefinedPatientId: $ fhirpath("QuestionnaireResponse.repeat(item).where(linkId='predefined-patient-id').answer.valueString").0
predefinedPatientDisplay: $ fhirpath("QuestionnaireResponse.repeat(item).where(linkId='patient').answer.valueString").0
predefinedStartDateTime: $ fhirpath("QuestionnaireResponse.repeat(item).where(linkId='start-datetime').answer.valueDateTime").0
selectedPractitionerRoleReference: $ fhirpath("QuestionnaireResponse.repeat(item).where(linkId='practitioner').answer.valueReference").0
selectedHealthcareServiceReference: $ fhirpath("QuestionnaireResponse.repeat(item).where(linkId='healthcare-service').answer.valueReference").0
$body:
$let:
selectedPractitionerRoleReferenceSplit:
$call: splitStr
$args:
- $ selectedPractitionerRoleReference.reference
- "/"
selectedHealthcareServiceReferenceSplit:
$call: splitStr
$args:
- $ selectedHealthcareServiceReference.reference
- "/"
$body:
resourceType: Bundle
type: transaction
entry:
- request:
url: /Appointment/$book
method: POST
resource:
resourceType: Bundle
entry:
- resource:
resourceType: Appointment
participant:
- actor:
resourceType: Patient
id: $ predefinedPatientId
display: $ predefinedPatientDisplay
status: accepted
- actor:
resourceType: PractitionerRole
id: $ selectedPractitionerRoleReferenceSplit.1
display: $ selectedPractitionerRoleReference.display
status: accepted
- actor:
resourceType: HealthcareService
id: $ selectedHealthcareServiceReferenceSplit.1
display: $ selectedHealthcareServiceReference.display
status: accepted
status: proposed
start: $ predefinedStartDateTime
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
resourceType: Questionnaire
id: new-appointment-proposed-patient
name: new-appointment-proposed-patient
title: New appointment proposed by patient
status: active
mapping:
- id: new-appointment-proposed-patient-extract
resourceType: Mapping
launchContext:
- name:
code: patient
type:
- Patient
description: Patient
- name:
code: appointment
type:
- Appointment
description: Appointment date passing from calendar
item:
- type: group
linkId: root-group
item:
- text: Service
type: reference
required: true
linkId: healthcare-service
referenceResource:
- HealthcareService
answerExpression:
language: application/x-fhir-query
expression: "HealthcareService"
choiceColumn:
- forDisplay: true
path: "name"
- text: Practitioner
type: reference
required: true
linkId: practitioner
referenceResource:
- PractitionerRole
answerExpression:
language: application/x-fhir-query
expression: "PractitionerRole?role=doctor&_assoc=practitioner"
choiceColumn:
- forDisplay: true
path: "practitioner.resource.name.given + ' ' + practitioner.resource.name.family + ' — ' + specialty.coding.display"
- text: Patient
type: string
hidden: false
readOnly: true
linkId: patient
initialExpression:
language: text/fhirpath
expression: "%patient.name.first().given.first() + ' ' + %patient.name.first().family.first()"
- text: Predefined Patient Id
type: string
linkId: predefined-patient-id
hidden: true
initialExpression:
language: text/fhirpath
expression: "%patient.id"
- text: Start time
required: true
type: dateTime
linkId: start-datetime
initialExpression:
language: text/fhirpath
expression: "%appointment.start"
meta:
profile:
- https://beda.software/beda-emr-questionnaire
url: https://aidbox.emr.beda.software/ui/console#/entities/Questionnaire/new-appointment-proposed-patient
1 change: 1 addition & 0 deletions src/containers/PatientDetails/PatientHeader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export function PatientHeader(props: { extraMenuItems?: RouteItem[]; isDefaultRo
{ label: t`Orders`, path: `/patients/${params.id}/orders` },
{ label: t`Smart Apps`, path: `/patients/${params.id}/apps` },
{ label: t`Resources`, path: `/patients/${params.id}/resources` },
{ label: t`Schedule`, path: `/patients/${params.id}/schedule` },
]
: []
).concat(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { t } from '@lingui/macro';
import { Appointment, Encounter } from 'fhir/r4b';

import { extractBundleResources, RenderRemoteData, useService } from '@beda.software/fhir-react';
import { mapSuccess } from '@beda.software/remote-data';

import { ReadonlyQuestionnaireResponseForm } from 'src/components/BaseQuestionnaireResponseForm/ReadonlyQuestionnaireResponseForm';
import { Modal } from 'src/components/Modal';
import { Spinner } from 'src/components/Spinner';
import { useNavigateToEncounter } from 'src/containers/EncounterDetails/hooks';
import { useStartEncounter } from 'src/containers/PatientDetails/PatientOverviewDynamic/components/StartEncounter/useStartEncounter';
import { getFHIRResources } from 'src/services/fhir';

interface Props {
appointmentId: string;
status: Appointment['status'];
onClose: () => void;
showModal: boolean;
}

function useAppointmentDetailsModal(props: Props) {
const { appointmentId } = props;
const { navigateToEncounter } = useNavigateToEncounter();
const { response: questionnaireResponseRD, onSubmit } = useStartEncounter(props);
const [encounterResponse] = useService(async () => {
const response = await getFHIRResources<Encounter>('Encounter', {
appointment: appointmentId,
});

return mapSuccess(response, (bundle) => {
const encounter = extractBundleResources(bundle).Encounter[0];

return { encounter };
});
});

return { encounterResponse, questionnaireResponseRD, onSubmit, navigateToEncounter };
}

export function AppointmentDetailsPatientModal(props: Props) {
const { showModal, onClose } = props;
const { questionnaireResponseRD } = useAppointmentDetailsModal(props);

return (
<Modal open={showModal} title={t`Appointment`} footer={null} onCancel={onClose} maskClosable={true}>
<RenderRemoteData remoteData={questionnaireResponseRD} renderLoading={Spinner}>
{(formData) => <ReadonlyQuestionnaireResponseForm formData={formData} />}
</RenderRemoteData>
</Modal>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Patient } from 'fhir/r4b';

import { questionnaireIdLoader } from 'shared/src/hooks/questionnaire-response-form-data';
import { formatFHIRDateTime } from 'shared/src/utils/date';

import { Modal } from 'src/components/Modal';
import { QuestionnaireResponseForm } from 'src/components/QuestionnaireResponseForm';

interface NewAppointmentModalProps {
patient: Patient;
start: Date;
showModal: boolean;
onOk?: () => void;
onCancel?: () => void;
}

export function NewAppointmentPatientModal(props: NewAppointmentModalProps) {
const { patient, showModal, start, onOk, onCancel } = props;
const appointmentStartDateTime = start ? formatFHIRDateTime(start) : formatFHIRDateTime(new Date());
// const end = moment(start).add(45, 'm').toDate();
// const appointmentEndDateTime = end ? formatFHIRDateTime(end) : formatFHIRDateTime(new Date());

return (
<Modal title="New Appointment" open={showModal} footer={null} onCancel={onCancel}>
<QuestionnaireResponseForm
onSuccess={onOk}
questionnaireLoader={questionnaireIdLoader('new-appointment-proposed-patient')}
launchContextParameters={[
{
name: 'patient',
resource: {
resourceType: 'Patient',
name: patient.name,
id: patient.id,
},
},
{
name: 'appointment',
resource: {
resourceType: 'Appointment',
start: appointmentStartDateTime,
status: 'proposed',
participant: [{ status: 'accepted' }],
},
},
]}
/>
</Modal>
);
}
146 changes: 146 additions & 0 deletions src/containers/PatientDetails/PatientSchedule/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
import FullCalendar from '@fullcalendar/react';
import timeGridPlugin from '@fullcalendar/timegrid';
import { t } from '@lingui/macro';
import { notification } from 'antd';
import { Appointment, Patient } from 'fhir/r4b';
import moment from 'moment';

import { getAllFHIRResources } from 'aidbox-react/lib/services/fhir';

import {
extractBundleResources,
formatFHIRDateTime,
RenderRemoteData,
useService,
WithId,
} from '@beda.software/fhir-react';
import { mapSuccess } from '@beda.software/remote-data';

import { Spinner } from 'src/components/Spinner';
import { AppointmentBubble } from 'src/containers/Scheduling/ScheduleCalendar';
import { useAppointmentEvents } from 'src/containers/Scheduling/ScheduleCalendar/hooks/useAppointmentEvents';
import { useCalendarOptions } from 'src/containers/Scheduling/ScheduleCalendar/hooks/useCalendarOptions';

import { AppointmentDetailsPatientModal } from './components/AppointmentDetailsPatientModal';
import { NewAppointmentPatientModal } from './components/NewAppointmentPatientModal';
import { S as SCalendar } from '../../../containers/Scheduling/ScheduleCalendar/ScheduleCalendar.styles';

interface Props {
patient: WithId<Patient>;
}

export function PatientSchedule(props: Props) {
const { calendarOptions } = useCalendarOptions();

const {
openNewAppointmentModal,
newAppointmentData,
closeNewAppointmentModal,
openAppointmentDetails,
appointmentDetails,
closeAppointmentDetails,
} = useAppointmentEvents();

const { patient } = props;

const periodStart = formatFHIRDateTime(moment().startOf('day').subtract(1, 'months'));
const periodEnd = formatFHIRDateTime(moment().endOf('day').add(1, 'months'));

const [appointments, appointmentsManager] = useService(async () => {
return mapSuccess(
await getAllFHIRResources<Appointment>('Appointment', {
date: [`ge${periodStart}`, `lt${periodEnd}`],
'actor:Patient._id': [patient.id],
}),
(bundle) => {
const resources = extractBundleResources(bundle);
const appointments = resources.Appointment;

return appointments.map((appointment) => {
return {
id: appointment.id,
title: appointment.serviceType![0]!.text!,
start: appointment.start!,
end: appointment.end!,
status: appointment.status,
classNames: [`_${appointment.status}`],
};
});
},
);
}, [periodStart, periodEnd]);

return (
<RenderRemoteData remoteData={appointments} renderLoading={Spinner}>
{(appointmentsList) => {
return (
<>
<SCalendar.Calendar>
<FullCalendar
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
nowIndicator={true}
headerToolbar={{
left: 'prev,next today',
center: 'title',
right: 'timeGridWeek,timeGridDay',
}}
editable={false}
selectable={true}
select={openNewAppointmentModal}
selectMirror={false}
initialView="timeGridWeek"
initialEvents={appointmentsList}
eventContent={AppointmentBubble}
eventClick={openAppointmentDetails}
buttonText={{
today: t`Today`,
week: t`Week`,
day: t`Day`,
}}
dayHeaderFormat={{
weekday: 'short',
day: 'numeric',
month: 'short',
}}
stickyHeaderDates={true}
allDaySlot={false}
contentHeight={650}
slotLabelFormat={{
timeStyle: 'short',
}}
{...calendarOptions}
/>
{appointmentDetails && (
<AppointmentDetailsPatientModal
key={`appointment-details__${appointmentDetails.id}`}
appointmentId={appointmentDetails.id}
status={appointmentDetails.extendedProps.status}
showModal={true}
onClose={closeAppointmentDetails}
/>
)}
{newAppointmentData && (
<NewAppointmentPatientModal
key={`new-appointment`}
patient={patient}
start={newAppointmentData.start}
showModal={true}
onOk={() => {
closeNewAppointmentModal();
appointmentsManager.reload();
notification.success({
message: t`Appointment successfully added`,
});
}}
onCancel={closeNewAppointmentModal}
/>
)}
</SCalendar.Calendar>
</>
);
}}
</RenderRemoteData>
);
}
Loading