Skip to content

Commit

Permalink
Merge branch 'dev' into issue-115
Browse files Browse the repository at this point in the history
  • Loading branch information
francisli committed Nov 1, 2024
2 parents 36ca898 + 2b053fb commit ad39085
Show file tree
Hide file tree
Showing 13 changed files with 532 additions and 36 deletions.
12 changes: 12 additions & 0 deletions client/src/pages/patients/LifelineAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ export default class LifelineAPI {
});
}

static async registerPhysician(data) {
const response = await fetch(`${SERVER_BASE_URL}/physicians`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});

return response;
}

static async getHospitals(query) {
const response = await fetch(
`${SERVER_BASE_URL}/hospitals?hospital=${query}`,
Expand Down
4 changes: 2 additions & 2 deletions client/src/pages/patients/register/PatientRegistration.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ export default function PatientRegistration() {
},
contactData: {
phone: (value) =>
value.length === 0 || value.match(/^\(\d{3}\)-\d{3}-\d{4}$/)
value.length === 0 || value.match(/^\(\d{3}\) \d{3}-\d{4}$/)
? null
: 'Phone number is not in XXX-XXX-XXXX format',
: 'Phone number is not in (XXX) XXX-XXXX format',
},
},
validateInputOnBlur: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ export default function PatientRegistrationAccordion({
<InputBase
label="Phone Number"
component={IMaskInput}
mask="(000)-000-0000"
placeholder="(000)-000-0000"
mask="(000) 000-0000"
placeholder="(000) 000-0000"
key={form.key('contactData.phone')}
{...form.getInputProps('contactData.phone')}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import PropTypes from 'prop-types';

import { useState, useRef, useEffect } from 'react';
import { Combobox, useCombobox, ScrollArea } from '@mantine/core';
import { Combobox, useCombobox, ScrollArea, Text } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { useDebouncedCallback } from '@mantine/hooks';
import { useDebouncedCallback, useDisclosure } from '@mantine/hooks';

import SearchDatabaseInputField from './SearchDatabaseInputField';
import RegisterPhysician from './RegisterPhysician';

import LifelineAPI from '../../LifelineAPI.js';

Expand All @@ -24,6 +25,10 @@ export default function HealthcareChoicesSearch({ form, choice, initialData }) {
const [data, setData] = useState([]);
const [empty, setEmpty] = useState(false);
const [search, setSearch] = useState('');
const [
registerPhysicianOpened,
{ open: openRegisterPhysician, close: closeRegisterPhysician },
] = useDisclosure(false);

const abortController = useRef();

Expand Down Expand Up @@ -70,8 +75,11 @@ export default function HealthcareChoicesSearch({ form, choice, initialData }) {
};

const handleSelectValue = (id, key) => {
const name = key.children;
setSearch(name.join(''));
if (id === '$register') {
return;
}
const name = key?.children?.join('') ?? key;
setSearch(name);
form.setFieldValue(`healthcareChoices.${choice}Id`, id);
combobox.closeDropdown();
};
Expand All @@ -90,7 +98,18 @@ export default function HealthcareChoicesSearch({ form, choice, initialData }) {
*/
function renderComboxContent() {
if (empty) {
return <Combobox.Empty>No results found</Combobox.Empty>;
return choice === 'physician' ? (
<>
<Combobox.Empty> No results found</Combobox.Empty>
<Combobox.Option value="$register" onClick={openRegisterPhysician}>
<Text fw={700} size="sm">
+ Register new physician
</Text>
</Combobox.Option>
</>
) : (
<Combobox.Empty>No results found</Combobox.Empty>
);
}

if (data.length === 0) {
Expand All @@ -100,24 +119,43 @@ export default function HealthcareChoicesSearch({ form, choice, initialData }) {
return (
<ScrollArea.Autosize type="scroll" mah={200}>
{options}
{choice === 'physician' && (
<Combobox.Option value="$register" onClick={openRegisterPhysician}>
<Text fw={700} size="sm">
+ Register new physician
</Text>
</Combobox.Option>
)}
</ScrollArea.Autosize>
);
}

return (
<SearchDatabaseInputField
data={data}
loading={loading}
combobox={combobox}
label={
choice === 'hospital' ? 'Hospital of Choice' : 'Preferred Care Provider'
}
searchQuery={search}
handleSelectValue={handleSelectValue}
fetchOptions={fetchOptions}
comboboxOptions={renderComboxContent}
handleSearch={handleSearch}
/>
<>
<SearchDatabaseInputField
data={data}
loading={loading}
combobox={combobox}
label={
choice === 'hospital'
? 'Hospital of Choice'
: 'Preferred Care Provider'
}
searchQuery={search}
handleSelectValue={handleSelectValue}
fetchOptions={fetchOptions}
comboboxOptions={renderComboxContent}
handleSearch={handleSearch}
/>
{choice === 'physician' && (
<RegisterPhysician
setPhysician={handleSelectValue}
registerPhysicianOpened={registerPhysicianOpened}
closeRegisterPhysician={closeRegisterPhysician}
fetchOptions={fetchOptions}
/>
)}
</>
);
}

Expand Down
217 changes: 217 additions & 0 deletions client/src/pages/patients/register/inputs/RegisterPhysician.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import PropTypes from 'prop-types';

import {
TextInput,
InputBase,
Button,
Alert,
Modal,
Transition,
Text,
} from '@mantine/core';
import { useForm, isNotEmpty } from '@mantine/form';
import { useDisclosure } from '@mantine/hooks';

import { IMaskInput } from 'react-imask';
import { useMutation } from '@tanstack/react-query';
import LifelineAPI from '../../LifelineAPI.js';
import { StatusCodes } from 'http-status-codes';

import classes from './RegisterPhysician.module.css';

const registerPhysicianProps = {
setPhysician: PropTypes.func.isRequired,
registerPhysicianOpened: PropTypes.bool.isRequired,
closeRegisterPhysician: PropTypes.func.isRequired,
fetchOptions: PropTypes.func.isRequired,
};

/**
*
* @param {PropTypes.InferProps<typeof registerPhysicianProps>} props
*/
export default function RegisterPhysician({
setPhysician,
registerPhysicianOpened,
closeRegisterPhysician,
fetchOptions,
}) {
const [
confirmationModalOpened,
{ open: openConfirmationModal, close: closeConfirmationModal },
] = useDisclosure(false);

const form = useForm({
mode: 'uncontrolled',
initialValues: {
firstName: '',
middleName: '',
lastName: '',
phone: '',
email: '',
},
validate: {
firstName: isNotEmpty('First Name is required'),
lastName: isNotEmpty('Last Name is required'),
phone: (value) =>
value.length > 0 || value.match(/^\(\d{3}\) \d{3}-\d{4}$/)
? null
: 'Phone number is not in (XXX) XXX-XXXX format',
email: (value) =>
value.length === 0 ||
value.match(/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/)
? null
: 'Email is not in valid format',
},
});

const {
error,
reset: mutationReset,
mutateAsync,
} = useMutation({
mutationKey: ['physician'],
mutationFn: async (data) => {
const res = await LifelineAPI.registerPhysician(data);
if (res.status === StatusCodes.CREATED) {
return await res.json();
} else {
const { message } = await res.json();
throw new Error(message);
}
},
});

const handleSubmit = async (values) => {
try {
const result = await mutateAsync(values);
const { firstName, middleName, lastName, phone } = result;
const fullName = `${firstName}${middleName ? ' ' + middleName + ' ' : ' '}${lastName}`;
setPhysician(result.id, `${fullName}${phone ? ` - ${phone}` : ''}`);
fetchOptions(fullName);
form.reset();
mutationReset();
closeRegisterPhysician();
} catch (error) {
console.error(error.message);
}
};

const confirmClose = (confirmed) => {
if (form.isDirty() && confirmed) {
closeConfirmationModal();
form.reset();
mutationReset();
}
if (form.isDirty()) {
openConfirmationModal();
} else {
form.reset();
closeRegisterPhysician();
}
};

return (
<>
<Modal
opened={registerPhysicianOpened}
onClose={confirmClose}
title="Register a new physician"
>
<form onSubmit={form.onSubmit(handleSubmit)}>
<Transition
mounted={error}
transition="slide-right"
duration={400}
timingFunction="ease"
>
{(transitionStyle) => (
<Alert
title="Failed to register physician."
color="red"
style={{ ...transitionStyle }}
>
{error?.message}
</Alert>
)}
</Transition>
<TextInput
label="First Name"
placeholder="First Name"
withAsterisk
key={form.key('firstName')}
{...form.getInputProps('firstName')}
/>
<TextInput
label="Middle Name"
placeholder="Middle Name"
key={form.key('middleName')}
{...form.getInputProps('middleName')}
/>
<TextInput
label="Last Name"
placeholder="Last Name"
withAsterisk
key={form.key('lastName')}
{...form.getInputProps('lastName')}
/>
<InputBase
label="Phone Number"
component={IMaskInput}
mask="(000) 000-0000"
placeholder="(000) 000-0000"
withAsterisk
key={form.key('phone')}
{...form.getInputProps('phone')}
/>
<TextInput
label="Email"
placeholder="Email"
format="email"
key={form.key('email')}
{...form.getInputProps('email')}
/>
<Button
style={{ marginTop: '1rem' }}
color="gray"
fullWidth
onClick={form.onSubmit(handleSubmit)}
>
Register Physician
</Button>
</form>
</Modal>
<Modal
opened={confirmationModalOpened}
onClose={closeConfirmationModal}
classNames={{
title: classes.title,
}}
title="This form has unsaved changes."
yOffset="16vh"
>
<Text fw={600}>
Are you sure you want to close this form without submitting?
</Text>
<Button
classNames={{ root: classes.button }}
color="red"
fullWidth
onClick={() => confirmClose(true)}
>
Yes
</Button>
<Button
classNames={{ root: classes.button }}
color="blue"
fullWidth
onClick={closeConfirmationModal}
>
No
</Button>
</Modal>
</>
);
}

RegisterPhysician.propTypes = registerPhysicianProps;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.title {
font-size: var(--mantine-font-size-xl);
font-weight: 600;
color: var(--mantine-color-red-6);
}

.button {
margin-top: 1rem;
}
Loading

0 comments on commit ad39085

Please sign in to comment.