-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* Create starter patients list page * Format files * Add a route to list all patients based on search term * Format files * Add tests for listing patients route * Provide default value for patient as empty string * Destructure options so include can be used with function The option include causes an error if passed as an argument for count() so it needs to be extracted from options * Update route to return additional necessary fields * Update tests * Add an API call to get patients * Basic display of all patients in a table format * Add some styling to table * Add a rounded border around table * Add action menu button at end of each row * Add header section * Link view profile button to patient profile page * Add icons to action menu options * Adjust rounding of border to avoid blank space * Utilize size prop from tabler icons * Refactor table component into separate component * Refactor table rows to be more dynamic * Adjust key value and style menu * Add JSDoc comment * Refactor API call for patients into separate file * Add search patients functionality * Add pagination to patients page * Return total pages from api results for pagination * Apply loading overlay to table component rather than page * Add an empty table state * Remove renewal and create buttons * Add a space between user first and last name * Add margin spacing between table and pagination * Add spacing between bottom of page and aain content * Add a scrollable container to the table * Increase responsiveness of grid layout on smaller screens * Change to native type so scroll area is hidden when not used * Reduce complexity of grid template column styling * Add a modal that confirms patient deletion if ADMIN user * Add a DELETE patient route * Remove use of array for role verification * Refactor usePatients hook * Add delete patient API calls * Add delete patient functionality from table * Explicitly return mutate function from useDeletePatient * Move table rows into its own component * Utilize async mutate fn and export isPending to help with loading states * Refactor table and utilize memoization * Remove unnecessary useMemo * Replace empty name fields with a dash * Fix typo * Allow api to return empty name fields from database * Style modal buttons * Format files * Fix typo in whereClause * Change use of p tag to Mantine Text component for consistency * Restyle modal text to be more appealing --------- Co-authored-by: Francis Li <[email protected]>
- Loading branch information
Showing
15 changed files
with
926 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import PropTypes from 'prop-types'; | ||
|
||
import { Link } from 'react-router-dom'; | ||
|
||
import { Table, Menu, ActionIcon } from '@mantine/core'; | ||
import { | ||
IconDotsVertical, | ||
IconUser, | ||
IconQrcode, | ||
IconTrash, | ||
} from '@tabler/icons-react'; | ||
|
||
const patientTableRowProps = { | ||
headers: PropTypes.arrayOf( | ||
PropTypes.shape({ | ||
key: PropTypes.string.isRequired, | ||
text: PropTypes.node, | ||
}), | ||
), | ||
patient: PropTypes.shape({ | ||
id: PropTypes.string.isRequired, | ||
name: PropTypes.string.isRequired, | ||
createdBy: PropTypes.string.isRequired, | ||
createdAt: PropTypes.string.isRequired, | ||
updatedBy: PropTypes.string.isRequired, | ||
updatedAt: PropTypes.string.isRequired, | ||
}), | ||
|
||
onDelete: PropTypes.func.isRequired, | ||
showDeleteMenu: PropTypes.bool.isRequired, | ||
}; | ||
|
||
/** | ||
* Patient table row component | ||
* @param {PropTypes.InferProps<typeof patientTableRowProps>} props | ||
*/ | ||
export default function PatientTableRow({ | ||
headers, | ||
patient, | ||
onDelete, | ||
showDeleteMenu, | ||
}) { | ||
return ( | ||
<Table.Tr key={patient.id}> | ||
{headers.map((header) => ( | ||
<Table.Td key={patient[header.key] + header.key}> | ||
{patient[header.key]} | ||
</Table.Td> | ||
))} | ||
<Table.Td> | ||
<Menu shadow="md"> | ||
<Menu.Target> | ||
<ActionIcon variant="subtle" color="gray"> | ||
<IconDotsVertical size={18} /> | ||
</ActionIcon> | ||
</Menu.Target> | ||
<Menu.Dropdown> | ||
<Menu.Item | ||
leftSection={<IconUser size={18} />} | ||
component={Link} | ||
to={`/patients/${patient.id}`} | ||
> | ||
View/Edit | ||
</Menu.Item> | ||
<Menu.Item leftSection={<IconQrcode size={18} />}> | ||
Reprint QR Code | ||
</Menu.Item> | ||
{showDeleteMenu && ( | ||
<Menu.Item | ||
leftSection={<IconTrash size={18} />} | ||
color="red" | ||
onClick={() => | ||
onDelete({ | ||
id: patient.id, | ||
name: patient.name, | ||
}) | ||
} | ||
> | ||
Delete | ||
</Menu.Item> | ||
)} | ||
</Menu.Dropdown> | ||
</Menu> | ||
</Table.Td> | ||
</Table.Tr> | ||
); | ||
} | ||
|
||
PatientTableRow.propTypes = patientTableRowProps; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { | ||
Container, | ||
Group, | ||
TextInput, | ||
Divider, | ||
Pagination, | ||
LoadingOverlay, | ||
Text, | ||
} from '@mantine/core'; | ||
import { useDebouncedCallback } from '@mantine/hooks'; | ||
import { useState } from 'react'; | ||
|
||
import { IconSearch } from '@tabler/icons-react'; | ||
|
||
import classes from './Patients.module.css'; | ||
import PatientsTable from './PatientsTable'; | ||
import { usePatients } from './usePatients'; | ||
|
||
/** | ||
* Patients page component | ||
* | ||
*/ | ||
export default function Patients() { | ||
const [inputValue, setInputValue] = useState(''); | ||
const { patients, headers, isFetching, page, pages, setPage, setSearch } = | ||
usePatients(); | ||
|
||
const handleSearch = useDebouncedCallback((query) => { | ||
setSearch(query); | ||
}, 500); | ||
|
||
return ( | ||
<Container> | ||
<div className={classes.header}> | ||
<Text fw={600} size="xl" mr="md"> | ||
Patients | ||
</Text> | ||
<Group> | ||
<TextInput | ||
leftSectionPointerEvents="none" | ||
leftSection={<IconSearch stroke={2} />} | ||
placeholder="Search" | ||
onChange={(event) => { | ||
setInputValue(event.currentTarget.value); | ||
handleSearch(event.currentTarget.value); | ||
}} | ||
value={inputValue} | ||
/> | ||
</Group> | ||
</div> | ||
<Divider mb="xl" /> | ||
<Container className={classes.patientsContainer}> | ||
<LoadingOverlay | ||
visible={isFetching} | ||
zIndex={1000} | ||
overlayProps={{ radius: 'sm', blur: 2 }} | ||
/> | ||
<PatientsTable headers={headers} data={patients} /> | ||
<Pagination total={pages} value={page} onChange={setPage} /> | ||
</Container> | ||
</Container> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
.tableWrapper { | ||
margin-bottom: var(--mantine-spacing-xs); | ||
} | ||
|
||
.table { | ||
border-radius: 7px; | ||
overflow: hidden; | ||
} | ||
|
||
.title { | ||
font-size: 1.2rem; | ||
font-weight: 600; | ||
color: var(--mantine-color-red-8); | ||
} | ||
|
||
.header { | ||
display: flex; | ||
justify-content: space-between; | ||
margin: 1rem 1rem; | ||
} | ||
|
||
.patientsContainer { | ||
position: relative; | ||
margin-bottom: var(--mantine-spacing-lg); | ||
} | ||
|
||
.button { | ||
margin-top: var(--mantine-spacing-lg); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import PropTypes from 'prop-types'; | ||
|
||
import { useState, useContext, useMemo, useCallback } from 'react'; | ||
import { Paper, Table, Modal, Button, Text } from '@mantine/core'; | ||
import { useDisclosure } from '@mantine/hooks'; | ||
import { useDeletePatient } from './useDeletePatient'; | ||
import { notifications } from '@mantine/notifications'; | ||
import classes from './Patients.module.css'; | ||
import Context from '../../Context'; | ||
|
||
import PatientTableRow from './PatientTableRow'; | ||
|
||
const patientTableProps = { | ||
headers: PropTypes.arrayOf( | ||
PropTypes.shape({ | ||
key: PropTypes.string.isRequired, | ||
text: PropTypes.node, | ||
}), | ||
), | ||
data: PropTypes.arrayOf( | ||
PropTypes.shape({ | ||
id: PropTypes.string.isRequired, | ||
name: PropTypes.string.isRequired, | ||
createdBy: PropTypes.string.isRequired, | ||
createdAt: PropTypes.string.isRequired, | ||
updatedBy: PropTypes.string.isRequired, | ||
updatedAt: PropTypes.string.isRequired, | ||
}), | ||
), | ||
}; | ||
|
||
/** | ||
* Patients table component | ||
* @param {PropTypes.InferProps<typeof patientTableProps>} props | ||
*/ | ||
export default function PatientsTable({ headers, data }) { | ||
const [opened, { open, close }] = useDisclosure(false); | ||
const [selectedPatient, setSelectedPatient] = useState(null); | ||
const { mutateAsync: deletePatient, isPending } = useDeletePatient(); | ||
const { user } = useContext(Context); | ||
|
||
const showDeleteConfirmation = useCallback( | ||
(patient) => { | ||
setSelectedPatient(patient); | ||
open(); | ||
}, | ||
[open], | ||
); | ||
const confirmPatientDeletion = async () => { | ||
try { | ||
await deletePatient(selectedPatient.id); | ||
notifications.show({ | ||
title: 'Success', | ||
message: 'Patient deleted successfully.', | ||
color: 'green', | ||
}); | ||
} catch (error) { | ||
console.error('Failed to delete patient:', error); | ||
notifications.show({ | ||
title: 'Error', | ||
message: 'Failed to delete patient.', | ||
color: 'red', | ||
}); | ||
} | ||
if (!isPending) { | ||
setSelectedPatient(null); | ||
close(); | ||
} | ||
}; | ||
|
||
const cancelPatientDeletion = () => { | ||
setSelectedPatient(null); | ||
close(); | ||
}; | ||
|
||
const emptyStateRow = useMemo( | ||
() => ( | ||
<Table.Tr> | ||
<Table.Td colSpan={headers.length}>No patients found.</Table.Td> | ||
</Table.Tr> | ||
), | ||
[headers.length], | ||
); | ||
|
||
const patientRows = useMemo(() => { | ||
return data?.map((patient) => ( | ||
<PatientTableRow | ||
key={patient.id} | ||
patient={patient} | ||
headers={headers} | ||
onDelete={showDeleteConfirmation} | ||
showDeleteMenu={user?.role === 'ADMIN'} | ||
/> | ||
)); | ||
}, [data, headers, user.role, showDeleteConfirmation]); | ||
|
||
return ( | ||
<> | ||
<Paper withBorder className={classes.tableWrapper}> | ||
<Table.ScrollContainer minWidth={500} type="native"> | ||
<Table | ||
stickyHeader | ||
highlightOnHover | ||
verticalSpacing="lg" | ||
classNames={{ table: classes.table }} | ||
> | ||
<Table.Thead> | ||
<Table.Tr> | ||
{headers.map((header) => ( | ||
<Table.Th key={header.key}>{header.text}</Table.Th> | ||
))} | ||
</Table.Tr> | ||
</Table.Thead> | ||
<Table.Tbody> | ||
{data?.length > 0 ? patientRows : emptyStateRow} | ||
</Table.Tbody> | ||
</Table> | ||
</Table.ScrollContainer> | ||
</Paper> | ||
<Modal | ||
opened={opened} | ||
onClose={close} | ||
title="Delete Patient" | ||
classNames={{ title: classes.title }} | ||
> | ||
<Text fw={600}> | ||
Are you sure you want to delete this patient record? | ||
</Text> | ||
<Button | ||
classNames={{ root: classes.button }} | ||
color="red" | ||
fullWidth | ||
onClick={confirmPatientDeletion} | ||
loading={isPending} | ||
> | ||
Yes | ||
</Button> | ||
<Button | ||
classNames={{ root: classes.button }} | ||
color="blue" | ||
fullWidth | ||
onClick={cancelPatientDeletion} | ||
disabled={isPending} | ||
> | ||
No | ||
</Button> | ||
</Modal> | ||
</> | ||
); | ||
} | ||
|
||
PatientsTable.propTypes = patientTableProps; |
Oops, something went wrong.