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

[Closes #115] Display a table of all patients #146

Merged
merged 58 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
35edd7a
Create starter patients list page
samau3 Oct 21, 2024
4015f14
Format files
samau3 Oct 21, 2024
3494e92
Add a route to list all patients based on search term
samau3 Oct 21, 2024
f781881
Format files
samau3 Oct 21, 2024
f83f123
Add tests for listing patients route
samau3 Oct 21, 2024
ccfae47
Provide default value for patient as empty string
samau3 Oct 21, 2024
d53b176
Destructure options so include can be used with function
samau3 Oct 21, 2024
a2dc1b6
Update route to return additional necessary fields
samau3 Oct 21, 2024
311d6bd
Update tests
samau3 Oct 21, 2024
14a2344
Add an API call to get patients
samau3 Oct 21, 2024
2037243
Basic display of all patients in a table format
samau3 Oct 21, 2024
7e1c71c
Add some styling to table
samau3 Oct 21, 2024
a4a7928
Add a rounded border around table
samau3 Oct 21, 2024
19355a1
Add action menu button at end of each row
samau3 Oct 22, 2024
f18079f
Add header section
samau3 Oct 22, 2024
0b779df
Link view profile button to patient profile page
samau3 Oct 22, 2024
5bcd020
Add icons to action menu options
samau3 Oct 22, 2024
08c0e63
Adjust rounding of border to avoid blank space
samau3 Oct 22, 2024
508b4d5
Utilize size prop from tabler icons
samau3 Oct 22, 2024
0116d28
Refactor table component into separate component
samau3 Oct 22, 2024
75b530b
Refactor table rows to be more dynamic
samau3 Oct 22, 2024
58d8997
Adjust key value and style menu
samau3 Oct 22, 2024
0574c8e
Add JSDoc comment
samau3 Oct 22, 2024
128e19d
Refactor API call for patients into separate file
samau3 Oct 22, 2024
3c17923
Add search patients functionality
samau3 Oct 24, 2024
2196ae5
Add pagination to patients page
samau3 Oct 24, 2024
c6fd18a
Return total pages from api results for pagination
samau3 Oct 24, 2024
f1cced1
Apply loading overlay to table component rather than page
samau3 Oct 24, 2024
6200574
Add an empty table state
samau3 Oct 24, 2024
30bf45f
Remove renewal and create buttons
samau3 Oct 25, 2024
1b10600
Add a space between user first and last name
samau3 Oct 25, 2024
065ca7c
Add margin spacing between table and pagination
samau3 Oct 25, 2024
296c525
Add spacing between bottom of page and aain content
samau3 Oct 25, 2024
4a53bcc
Add a scrollable container to the table
samau3 Oct 26, 2024
edd07c3
Increase responsiveness of grid layout on smaller screens
samau3 Oct 26, 2024
01e3d9c
Change to native type so scroll area is hidden when not used
samau3 Oct 26, 2024
e72c4b1
Reduce complexity of grid template column styling
samau3 Oct 26, 2024
826e121
Add a modal that confirms patient deletion if ADMIN user
samau3 Oct 26, 2024
4f98f12
Add a DELETE patient route
samau3 Oct 27, 2024
2edd0ec
Remove use of array for role verification
samau3 Oct 27, 2024
094ea6b
Refactor usePatients hook
samau3 Oct 27, 2024
4c62a9b
Add delete patient API calls
samau3 Oct 27, 2024
2015779
Add delete patient functionality from table
samau3 Oct 27, 2024
b7eb688
Explicitly return mutate function from useDeletePatient
samau3 Oct 27, 2024
629fd2e
Move table rows into its own component
samau3 Oct 27, 2024
ab089de
Utilize async mutate fn and export isPending to help with loading states
samau3 Oct 27, 2024
45996d2
Refactor table and utilize memoization
samau3 Oct 27, 2024
754174f
Remove unnecessary useMemo
samau3 Oct 27, 2024
4063ede
Replace empty name fields with a dash
samau3 Oct 27, 2024
e647d34
Fix typo
samau3 Oct 27, 2024
abad3f7
Allow api to return empty name fields from database
samau3 Oct 27, 2024
9165fb8
Style modal buttons
samau3 Oct 27, 2024
1e5da93
Format files
samau3 Oct 27, 2024
eaa60d7
Merge dev into issue-115
samau3 Oct 27, 2024
a7b7a60
Fix typo in whereClause
samau3 Oct 27, 2024
aca5cc7
Change use of p tag to Mantine Text component for consistency
samau3 Oct 28, 2024
36ca898
Restyle modal text to be more appealing
samau3 Oct 28, 2024
ad39085
Merge branch 'dev' into issue-115
francisli Nov 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import AuthLayout from './stories/AuthLayout/AuthLayout';
import Verify from './pages/verify/verify';
import PatientRegistration from './pages/patients/register/PatientRegistration';
import PatientDetails from './pages/patients/patient-details/PatientDetails';
import Patients from './pages/patients/Patients';

const RedirectProps = {
isLoading: PropTypes.bool.isRequired,
Expand Down Expand Up @@ -140,6 +141,7 @@ function App() {
element={<AdminPatientsGenerate />}
/>
<Route element={<Layout />}>
<Route path="/patients" element={<Patients />} />
<Route path="/patients/:patientId" element={<PatientDetails />} />
<Route
path="/patients/register/:patientId"
Expand Down
6 changes: 5 additions & 1 deletion client/src/components/Sidebar/Sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ const sections = [
label: 'Management',
icon: null,
links: [
{ label: 'Patient', href: '/', icon: <IconEmergencyBed stroke={2} /> },
{
label: 'Patients',
href: '/patients',
icon: <IconEmergencyBed stroke={2} />,
},
{
label: 'Team Member',
href: '/admin/users',
Expand Down
14 changes: 14 additions & 0 deletions client/src/pages/patients/LifelineAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ export default class LifelineAPI {
return response;
}

static async getPatients(query, page) {
const response = await fetch(
`${SERVER_BASE_URL}/patients?patient=${query}&page=${page}`,
);
return response;
}

static async registerPatient(data, patientId) {
const response = await fetch(`${SERVER_BASE_URL}/patients`, {
method: 'POST',
Expand All @@ -71,6 +78,13 @@ export default class LifelineAPI {
return response;
}

static async deletePatient(patientId) {
const response = await fetch(`${SERVER_BASE_URL}/patients/${patientId}`, {
method: 'DELETE',
});
return response;
}

static async getMedicalData(path, pathInfo, query) {
const response = await fetch(
`${SERVER_BASE_URL}/${path}?${pathInfo}=${query}`,
Expand Down
89 changes: 89 additions & 0 deletions client/src/pages/patients/PatientTableRow.jsx
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;
63 changes: 63 additions & 0 deletions client/src/pages/patients/Patients.jsx
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>
);
}
29 changes: 29 additions & 0 deletions client/src/pages/patients/Patients.module.css
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);
}
152 changes: 152 additions & 0 deletions client/src/pages/patients/PatientsTable.jsx
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;
Loading