Skip to content

Commit

Permalink
feat: appointment details during scheduling
Browse files Browse the repository at this point in the history
mgramigna committed Dec 16, 2023
1 parent 857379a commit a55d6bd
Showing 10 changed files with 213 additions and 34 deletions.
4 changes: 0 additions & 4 deletions apps/expo/src/app/(app)/home/index.tsx
Original file line number Diff line number Diff line change
@@ -47,10 +47,6 @@ const Home = () => {
},
);

console.log({
medicationStatementBundle,
});

if (isLoading) {
return (
<ScreenView>
26 changes: 26 additions & 0 deletions apps/expo/src/components/atoms/RadioButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { TouchableWithoutFeedback, View } from 'react-native';
import { cn } from '@/utils/cn';

import { Text } from './Text';

export interface RadioButtonProps {
selected?: boolean;
label?: string;
onPress?: () => void;
}

export const RadioButton = ({ label, selected, onPress }: RadioButtonProps) => {
return (
<TouchableWithoutFeedback onPress={onPress}>
<View className="flex flex-row items-center">
<View
className={cn(
'border-coolGray-200 h-8 w-8 rounded-full border bg-none',
selected && 'bg-cyan-300',
)}
/>
<Text className="ml-2">{label}</Text>
</View>
</TouchableWithoutFeedback>
);
};
146 changes: 137 additions & 9 deletions apps/expo/src/components/molecules/ScheduleAppointmentModal.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,72 @@
import { Modal, View } from 'react-native';
import { useCallback } from 'react';
import { ActivityIndicator, Modal, View } from 'react-native';
import { getLocationAddress } from '@/fhirpath/location';
import { getPractitionerName } from '@/fhirpath/practitioner';
import { usePractitioner } from '@/hooks/usePractitioner';
import { api } from '@/utils/api';
import { HARDCODED_OFFICE_LOCATION_ID } from '@/utils/constants';
import dayjs from 'dayjs';
import { Controller, useForm } from 'react-hook-form';
import { z } from 'zod';

import { type Slot } from '@canvas-challenge/canvas';

import { Button } from '../atoms/Button';
import { RadioButton } from '../atoms/RadioButton';
import { Text } from '../atoms/Text';
import { TextInput } from '../atoms/TextInput';
import { ScreenView } from './ScreenView';

const ConfirmAppointmentFormSchema = z.object({
reasonText: z.string().optional(),
appointmentType: z.enum(['office', 'telehealth']),
});

type ConfirmAppointmentForm = z.infer<typeof ConfirmAppointmentFormSchema>;

export const ScheduleAppointmentModal = ({
slot,
isOpen,
onClose,
onConfirm,
practitionerId,
isConfirming,
}: {
isConfirming?: boolean;
practitionerId: string;
slot: Slot;
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
onConfirm: (appointmentType: 'office' | 'telehealth', reasonText?: string) => void;
}) => {
const { practitioner, isLoading } = usePractitioner(practitionerId);

const { data: location } = api.location.get.useQuery({
// TODO: remove this when Canvas fixes bug with location IDs
id: HARDCODED_OFFICE_LOCATION_ID,
});

const onSubmit = useCallback(
(form: ConfirmAppointmentForm) => {
onConfirm(form.appointmentType, form.reasonText === '' ? undefined : form.reasonText);
},
[onConfirm],
);

const {
formState: { isValid },
control,
handleSubmit,
watch,
} = useForm<ConfirmAppointmentForm>({
defaultValues: {
appointmentType: 'office',
reasonText: '',
},
});

const appointmentType = watch('appointmentType');

return (
<Modal
animationType="slide"
@@ -25,13 +75,91 @@ export const ScheduleAppointmentModal = ({
onRequestClose={onClose}
>
<ScreenView>
<View className="h-full">
<Text>TODO: schedule appt</Text>
<Text>
{slot.start} - {slot.end}
</Text>
<Button text="Confirm Appointment" onPress={onConfirm} />
<Button variant={'secondary'} text="Cancel" onPress={onClose} />
<View className="flex h-full">
{isLoading ? (
<ActivityIndicator />
) : (
<>
<View>
<Text className="text-3xl" weight="bold">
Confirm Your Appointment
</Text>
<Text className="mt-8 text-xl">
<Text weight="bold">{dayjs(slot.start).format('ddd MMM DD, YYYY')}</Text> from{' '}
<Text weight="bold">
{dayjs(slot.start).format('hh:mm')} - {dayjs(slot.end).format('hh:mm a')}
</Text>{' '}
with{' '}
<Text weight="bold">
{practitioner ? getPractitionerName(practitioner) : 'Your Care Team'}
</Text>
</Text>
</View>
<View className="mt-8">
<Text className="mb-2 pl-1 text-xl">
In a few words, tell us what you'd like to focus on in your appointment (E.g.
"general check up", "back pain")
</Text>
<Controller
control={control}
name="reasonText"
render={({ field: { onChange, onBlur, value } }) => (
<TextInput
placeholder="Enter reason for visit..."
onBlur={onBlur}
onChangeText={onChange}
value={value}
multiline
className="h-24"
/>
)}
/>
</View>
<View className="mt-8">
<Text className="mb-2 pl-1 text-xl">
Would you prefer an office visit in the clinic, or a telehealth visit?
</Text>
<View className="flex flex-row justify-evenly">
<Controller
control={control}
name="appointmentType"
render={({ field: { onChange } }) => (
<RadioButton
label="Office"
onPress={() => onChange('office')}
selected={appointmentType === 'office'}
/>
)}
/>
<Controller
control={control}
name="appointmentType"
render={({ field: { onChange } }) => (
<RadioButton
label="Telehealth"
onPress={() => onChange('telehealth')}
selected={appointmentType === 'telehealth'}
/>
)}
/>
</View>
</View>
{location && appointmentType === 'office' && (
<View className="mt-2">
<Text className="text-center">{getLocationAddress(location)}</Text>
</View>
)}
<View className="mt-8 flex gap-4">
<Button
text="Confirm Appointment"
onPress={handleSubmit(onSubmit)}
disabled={!isValid}
isLoading={isConfirming}
/>
<Button variant={'secondary'} text="Cancel" onPress={onClose} />
</View>
</>
)}
</View>
</ScreenView>
</Modal>
27 changes: 16 additions & 11 deletions apps/expo/src/components/organisms/SlotDetail.tsx
Original file line number Diff line number Diff line change
@@ -29,18 +29,21 @@ export const SlotDetail = ({
},
});

const handleAppointmentConfirm = useCallback(() => {
const appointment = getAppointmentResource({
practitionerId,
patientId,
appointmentType: 'office', // TODO
reasonText: 'testing auto creation',
start: slot.start,
end: slot.end,
});
const handleAppointmentConfirm = useCallback(
(appointmentType: 'office' | 'telehealth', reasonText?: string) => {
const appointment = getAppointmentResource({
practitionerId,
patientId,
appointmentType,
reasonText,
start: slot.start,
end: slot.end,
});

createAppointment.mutate(appointment);
}, [slot, createAppointment, practitionerId, patientId]);
createAppointment.mutate(appointment);
},
[slot, createAppointment, practitionerId, patientId],
);

return (
<>
@@ -52,6 +55,8 @@ export const SlotDetail = ({
/>
</View>
<ScheduleAppointmentModal
practitionerId={practitionerId}
isConfirming={createAppointment.status === 'pending'}
onConfirm={handleAppointmentConfirm}
onClose={() => setScheduleModalOpen(false)}
isOpen={scheduleModalOpen}
15 changes: 15 additions & 0 deletions apps/expo/src/fhirpath/location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import fhirpath from 'fhirpath';

import { type Location } from '@canvas-challenge/canvas';

export function getLocationAddress(location: Location): string {
const [street] = fhirpath.evaluate(location, 'address.line') as string[];

const [city] = fhirpath.evaluate(location, 'address.city') as string[];

const [state] = fhirpath.evaluate(location, 'address.state') as string[];

const [postalCode] = fhirpath.evaluate(location, 'address.postalCode') as string[];

return `${street} ${city}, ${state} ${postalCode}`;
}
9 changes: 9 additions & 0 deletions apps/expo/src/hooks/usePractitioner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { api } from '@/utils/api';

export function usePractitioner(practitionerId: string) {
const { data: practitioner, isLoading } = api.practitioner.get.useQuery({
id: practitionerId,
});

return { practitioner, isLoading };
}
1 change: 1 addition & 0 deletions apps/expo/src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const HARDCODED_OFFICE_LOCATION_ID = '83bb9465-b895-4539-898f-bb4b87aff340';
export const HARDCODED_OFFICE_LOCATION_ID_FOR_CREATE = '1';
16 changes: 7 additions & 9 deletions apps/expo/src/utils/fhir.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type Appointment, type Communication } from '@canvas-challenge/canvas';

import { HARDCODED_OFFICE_LOCATION_ID } from './constants';
import { HARDCODED_OFFICE_LOCATION_ID_FOR_CREATE } from './constants';

export function getIdPartFromReference(reference: string): string {
const [_resourceType, idPart] = reference.split('/');
@@ -78,14 +78,12 @@ export function getAppointmentResource({
},
],
},
...(appointmentType === 'office' && {
supportingInformation: [
{
reference: `Location/${HARDCODED_OFFICE_LOCATION_ID}`,
type: 'Location',
},
],
}),
supportingInformation: [
{
reference: `Location/${HARDCODED_OFFICE_LOCATION_ID_FOR_CREATE}`,
type: 'Location',
},
],
...(reasonText && {
reasonCode: [
{
2 changes: 1 addition & 1 deletion apps/nextjs/package.json
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@
"scripts": {
"build": "pnpm with-env next build",
"clean": "git clean -xdf .next .turbo node_modules",
"dev": "pnpm with-env next dev",
"dev": "pnpm with-env next dev -- -p 3001",
"lint": "dotenv -v SKIP_ENV_VALIDATION=1 next lint",
"format": "prettier --check . --ignore-path ../../.gitignore",
"start": "pnpm with-env next start",
1 change: 1 addition & 0 deletions tooling/eslint/react.js
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ const config = {
],
rules: {
'react/prop-types': 'off',
'react/no-unescaped-entities': 'off',
},
globals: {
React: 'writable',

0 comments on commit a55d6bd

Please sign in to comment.